hook_dsl Module
Domain-Specific Language for simulation hooks.
Overview
The hook_dsl module provides the core components for the declarative hook system.
Complete Module Reference
natal.hook_dsl
Backward-compatible hook DSL imports.
The implementation now lives under natal.hooks.
This module remains as a stable compatibility layer for existing imports.
CompiledEventHooks
Container for event-wise combined hook callables.
Kernel code expects one callable per event name. This class stores those
callables and optionally the declarative HookProgram registry.
When hooks are present and Numba is enabled, lifecycle wrappers are
pre-compiled with hooks as globals so Numba caching survives restarts.
Source code in src/natal/hooks/compiler.py
from_compiled_hooks
staticmethod
from_compiled_hooks(compiled_hooks: List[CompiledHookDescriptor], registry: Optional[HookProgram] = None, include_spatial_wrappers: bool = False) -> CompiledEventHooks
Build event-wise combined callables and lifecycle wrappers.
Unlike the previous Jinja2-codegen approach, this method generates
only the necessary lifecycle wrapper per hook combination using
compile_lifecycle_wrapper, which produces a uniquely-named njit
function with hooks as globals. This ensures Numba cache=True
works across process restarts.
Source code in src/natal/hooks/compiler.py
Op
Factory helpers for building declarative operations.
The methods here only build data objects and do not touch population state.
Compilation happens later in compile_declarative_hook.
scale
staticmethod
scale(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', factor: float = 1.0, when: Optional[str] = None) -> HookOp
Create a scaling operation that multiplies counts by a factor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector ("*" for all, specific genotype, or list) |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector ("*" for all, specific age, range, or list) |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector ("female", "male", or "both") |
'both'
|
factor
|
float
|
Scaling factor (e.g., 0.5 halves the count, 2.0 doubles it) |
1.0
|
when
|
Optional[str]
|
Optional condition expression (e.g., "tick >= 100") |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
set_count
staticmethod
set_count(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', value: float = 0.0, when: Optional[str] = None) -> HookOp
Create an operation that sets counts to a specific value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
value
|
float
|
Target count value (individuals will be added/removed to match) |
0.0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
add
staticmethod
add(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', delta: float = 0.0, when: Optional[str] = None) -> HookOp
Create an operation that adds a fixed number of individuals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
delta
|
float
|
Number of individuals to add (can be negative to remove) |
0.0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
subtract
staticmethod
subtract(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', delta: float = 0.0, when: Optional[str] = None) -> HookOp
Create an operation that subtracts a fixed number of individuals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
delta
|
float
|
Number of individuals to subtract |
0.0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
kill
staticmethod
kill(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', prob: float = 0.0, when: Optional[str] = None) -> HookOp
Create a probabilistic killing operation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
prob
|
float
|
Probability of killing each selected individual (0.0 to 1.0) |
0.0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Raises:
| Type | Description |
|---|---|
ValueError
|
If probability is not in [0, 1] |
Source code in src/natal/hooks/declarative.py
sample
staticmethod
sample(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', size: int = 0, when: Optional[str] = None) -> HookOp
Create a sampling operation that selects individuals without replacement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
size
|
int
|
Number of individuals to sample |
0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
stop_if_zero
staticmethod
stop_if_zero(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', when: Optional[str] = None) -> HookOp
Create an operation that stops the simulation if selected count reaches zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
stop_if_below
staticmethod
stop_if_below(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', threshold: float = 1.0, when: Optional[str] = None) -> HookOp
Create an operation that stops the simulation if count falls below threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
threshold
|
float
|
Minimum count threshold |
1.0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
stop_if_above
staticmethod
stop_if_above(genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', threshold: float = 1000000.0, when: Optional[str] = None) -> HookOp
Create an operation that stops the simulation if count exceeds threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genotypes
|
Union[str, List[str], Literal['*']]
|
Genotype selector |
'*'
|
ages
|
Union[int, List[int], range, Literal['*']]
|
Age selector |
'*'
|
sex
|
Literal['female', 'male', 'both']
|
Sex selector |
'both'
|
threshold
|
float
|
Maximum count threshold |
1000000.0
|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
stop_if_extinction
staticmethod
Create an operation that stops the simulation if total population goes extinct.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
when
|
Optional[str]
|
Optional condition expression |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
HookOp |
HookOp
|
Operation descriptor for compilation |
Source code in src/natal/hooks/declarative.py
HookExecutor
Python-layer coordinator for all hook execution modes.
This class is used by population event dispatch where both njit and Python callback hooks must coexist around the declarative CSR core.
Source code in src/natal/hooks/executor.py
from_compiled_hooks
staticmethod
from_compiled_hooks(registry: HookProgram, compiled_hooks: List[CompiledHookDescriptor]) -> HookExecutor
Group descriptors by event and sort by priority.
Source code in src/natal/hooks/executor.py
execute_event
Run one event with priority ordering across hook types.
Source code in src/natal/hooks/executor.py
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | |
CompiledHookDescriptor
dataclass
CompiledHookDescriptor(name: str, event: str, priority: int = 0, deme_selector: DemeSelector = '*', plan: Optional[CompiledHookPlan] = None, selectors: Dict[str, ndarray] = _empty_selector_map(), static_arrays: Tuple[ndarray, ...] = tuple(), meta: Dict[str, int] = _empty_meta_map(), njit_fn: Optional[Callable[..., object]] = None, py_wrapper: Optional[Callable[..., object]] = None, ops: Optional[List[HookOp]] = None)
Unified descriptor for all hook modes.
Exactly one of plan, njit_fn, or py_wrapper is typically used
as the primary execution payload for a descriptor.
CompiledHookPlan
dataclass
CompiledHookPlan(n_ops: int, op_types: ndarray, gidx_offsets: ndarray, gidx_data: ndarray, age_offsets: ndarray, age_data: ndarray, sex_masks: ndarray, params: ndarray, condition_offsets: ndarray, condition_types: ndarray, condition_params: ndarray)
Compiled declarative plan with CSR-style flattened arrays.
Variable-length fields (genotypes/ages/conditions) are represented via
*_offsets + *_data to keep kernel inputs contiguous and compact.
HookOp
dataclass
HookOp(op_type: OpType, genotypes: Union[str, List[str], Literal['*']] = '*', ages: Union[int, List[int], range, Literal['*']] = '*', sex: Literal['female', 'male', 'both'] = 'both', param: float = 1.0, condition: Optional[str] = None)
Single declarative operation before compilation.
Fields in this class can still be symbolic (for example genotype labels). The compiler resolves all symbolic fields into concrete integer arrays.
HookProgram
Bases: NamedTuple
Event-grouped plain-data CSR representation for declarative hooks.
OpType
Bases: IntEnum
Operation opcodes consumed by the runtime kernel.
We intentionally keep integer values stable because these values are
serialized into CompiledHookPlan.op_types and interpreted in the
executor hot-loop.
compile_combined_hook
compile_combined_hook(njit_fns: List[HookCallable], deme_selectors: Optional[List[DemeSelector]] = None) -> HookCallable
Combine multiple njit hooks into one generated njit function.
We generate source code instead of composing Python closures so the result remains callable from njit kernels.
When deme_selectors is provided and contains non-wildcard values,
each hook call is wrapped with an if deme_id == X guard so that
per-deme hooks only execute for their target deme(s) — critical for
spatial simulations where all hooks share one combined function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
njit_fns
|
List[HookCallable]
|
List of njit-compiled hook functions. |
required |
deme_selectors
|
Optional[List[DemeSelector]]
|
Optional per-function deme target. When |
None
|
Source code in src/natal/hooks/compiler.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
hook
hook(event: Optional[str] = None, selectors: Optional[Dict[str, Any]] = None, priority: int = 0, custom: bool = False, deme: DemeSelector = '*') -> Callable[[Callable[..., Any]], DecoratedHookFn]
Decorator entrypoint for all supported hook authoring styles.
The decorated function gets a register(pop, event_override=None)
helper that compiles and registers a CompiledHookDescriptor.
Hook type is determined by: - selectors specified -> Selector hook - custom=True or has required params -> Custom hook - otherwise -> Declarative hook (function returns List[HookOp])
For custom/selector hooks, Numba compilation is automatic — you do
not need to stack @njit. If Numba is enabled, the function is
wrapped with njit_switch automatically. If Numba is disabled, a
pure-Python wrapper is used.
When a custom hook is called inside a spatial prange region, the
deme_id parameter receives the current deme index, enabling one
hook function to handle all demes with per-deme branching logic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
Optional[str]
|
Hook event name. |
None
|
selectors
|
Optional[Dict[str, Any]]
|
Optional symbolic selectors for selector-mode hooks. |
None
|
priority
|
int
|
Execution priority (lower values run earlier). |
0
|
custom
|
bool
|
If True, treat as custom hook (function is called directly). |
False
|
deme
|
DemeSelector
|
Target deme(s) for spatial populations. |
'*'
|
Source code in src/natal/hooks/compiler.py
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 | |
compile_declarative_hook
compile_declarative_hook(ops: List[HookOp], pop: BasePopulation[Any], event: str, priority: int = 0, deme_selector: DemeSelector = '*', name: str = 'declarative_hook') -> CompiledHookDescriptor
Compile declarative ops into a CompiledHookDescriptor.
The compiler packs all per-op fields into parallel arrays. Offsets arrays
(*_offsets) define CSR spans for variable-length selector/condition
data and avoid Python object usage in runtime kernels.
Source code in src/natal/hooks/declarative.py
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 | |
build_hook_program
deme_selector_matches
Return whether one deme id should execute under selector.
Supported forms: - "*" for all demes - int for one deme - list/tuple/range for a set of demes
Source code in src/natal/hooks/executor.py
execute_csr_event_arrays
execute_csr_event_arrays(n_events: int | integer[Any], n_hooks: int | integer[Any], hook_offsets: ndarray, n_ops_list: ndarray, op_offsets: ndarray, op_types_data: ndarray, gidx_offsets_data: ndarray, gidx_data: ndarray, age_offsets_data: ndarray, age_data: ndarray, sex_masks_data: ndarray, params_data: ndarray, condition_offsets_data: ndarray, condition_types_data: ndarray, condition_params_data: ndarray, deme_selector_types: ndarray, deme_selector_offsets: ndarray, deme_selector_data: ndarray, event_id: int, individual_count: ndarray, sperm_storage: ndarray, has_sperm_storage: bool, tick: int, is_stochastic: bool, use_continuous_sampling: bool, deme_id: int) -> int
Execute one event from flattened CSR arrays.
Inputs are plain arrays extracted from HookProgram. This function is
the hottest part of declarative hook runtime.
Source code in src/natal/hooks/executor.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | |
execute_csr_event_program
execute_csr_event_program(program: HookProgram, event_id: int, individual_count: ndarray, tick: int) -> int
Compatibility wrapper with deterministic defaults and no sperm storage.
Source code in src/natal/hooks/executor.py
execute_csr_event_program_with_state
execute_csr_event_program_with_state(program: HookProgram, event_id: int, individual_count: ndarray, sperm_storage: ndarray, tick: int, is_stochastic: bool, has_sperm_storage: bool, use_continuous_sampling: bool, deme_id: int = 0) -> int
Execute event directly from HookProgram while exposing state flags.
Source code in src/natal/hooks/executor.py
compile_selector_hook
compile_selector_hook(func: Callable[..., Any], pop: BasePopulation[Any], event: str, selectors_spec: Dict[str, SelectorSpec], priority: int = 0, deme_selector: DemeSelector = '*') -> CompiledHookDescriptor
Compile selector hook into njit or python descriptor.
resolved stores canonical selector arrays and is reused by both
execution paths.
For selector hooks, Numba compilation depends on: - If function is @njit decorated, use it directly - Otherwise, use global NUMBA_ENABLED setting (auto-wrap if enabled)
Source code in src/natal/hooks/selector.py
njit_switch
njit_switch(func: Optional[Callable[P, R]] = None, *, cache: bool = True, parallel: bool = False, fastmath: bool = False, **njit_kwargs: Any) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]
Numba @njit decorator for functions (controlled by global NUMBA_ENABLED flag).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Optional[Callable[P, R]]
|
Function to decorate |
None
|
cache
|
bool
|
Cache compiled functions (default: True) |
True
|
parallel
|
bool
|
Enable automatic parallelization (default: False) |
False
|
fastmath
|
bool
|
Enable fast math optimizations (default: False) |
False
|
**njit_kwargs
|
Any
|
Additional arguments for numba.njit |
{}
|
Examples:
@njit_switch
def my_func(x):
...
@njit_switch(parallel=True, fastmath=True)
def my_parallel_func(x):
...