Skip to content

Rule Systems

Composable structural rule systems for chart parsing. A RuleSystem stores binary and unary inference rules as index tensors. The + operator merges rule systems for hybrid grammars.

A rule system may carry learnable weights, one log weight per binary rule and one per unary rule. Parsing adds these weights to the chart combination scores. When passed to a ChartParser, the weights initialize its nn.Parameter tensors, or fixed buffers when learnable_rule_weights=False. Merging with + preserves each rule's weight; duplicate rules keep the weight from the left operand.

The convenience functions ccg_rules and lambek_rules instantiate rule schema presets over a given category system. For custom grammars, compose the schema primitives directly via |.

_rule_system

Core RuleSystem model for composable chart parsing rules.

This private module exists to break circular imports between biclosed.py (primitive categorical operations) and rules.py (grammar-specific rule combinations).

Each rule may carry an optional initial log-weight. When registered in a ChartParser, these become learnable nn.Parameter values that are added to rule scores during chart filling, implementing a weighted deductive system.

RuleSystem didactic-model

RuleSystem(*, binary_rules: tuple[tuple[int, ...], ...] = (), unary_rules: tuple[tuple[int, ...], ...] = (), n_categories: int = 0, description: str = '', binary_weights: tuple[float, ...] | None = None, unary_weights: tuple[float, ...] | None = None)

Bases: Model

A composable set of structural rules for chart parsing.

ATTRIBUTE DESCRIPTION
binary_rules

Each inner tuple is (result_idx, left_idx, right_idx).

TYPE: tuple[tuple[int, ...], ...]

unary_rules

Each inner tuple is (result_idx, input_idx).

TYPE: tuple[tuple[int, ...], ...]

n_categories

Total number of categories in the system.

TYPE: int

description

Human-readable label (e.g. "CCG", "Lambek(L)").

TYPE: str

binary_weights

Initial log-weights for binary rules. None means all rules are unweighted (weight 0 in log-space). When supplied, length must match binary_rules.

TYPE: tuple[float, ...] | None

unary_weights

Initial log-weights for unary rules. None means all rules are unweighted. When supplied, length must match unary_rules.

TYPE: tuple[float, ...] | None

n_binary property

n_binary: int

Number of binary rules.

n_unary property

n_unary: int

Number of unary rules.

has_weights property

has_weights: bool

Whether any rules carry explicit weights.

binary_tensors

binary_tensors(device: device | None = None) -> tuple[Tensor, Tensor, Tensor]

Return binary rules as (results, lefts, rights) index tensors.

Source code in src/quivers/stochastic/_rule_system.py
def binary_tensors(
    self,
    device: torch.device | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    """Return binary rules as (results, lefts, rights) index tensors."""
    if not self.binary_rules:
        empty = torch.zeros(0, dtype=torch.long, device=device)
        return empty, empty, empty

    results, lefts, rights = zip(*self.binary_rules)

    return (
        torch.tensor(results, dtype=torch.long, device=device),
        torch.tensor(lefts, dtype=torch.long, device=device),
        torch.tensor(rights, dtype=torch.long, device=device),
    )

binary_weight_tensor

binary_weight_tensor(device: device | None = None) -> Tensor

Return initial binary rule weights as a float tensor.

Returns zeros when no explicit weights are set.

Source code in src/quivers/stochastic/_rule_system.py
def binary_weight_tensor(
    self,
    device: torch.device | None = None,
) -> torch.Tensor:
    """Return initial binary rule weights as a float tensor.

    Returns zeros when no explicit weights are set.
    """
    if self.binary_weights is not None:
        return torch.tensor(
            self.binary_weights,
            dtype=torch.float,
            device=device,
        )

    return torch.zeros(self.n_binary, dtype=torch.float, device=device)

unary_tensors

unary_tensors(device: device | None = None) -> tuple[Tensor, Tensor] | None

Return unary rules as (results, inputs) index tensors, or None.

Source code in src/quivers/stochastic/_rule_system.py
def unary_tensors(
    self,
    device: torch.device | None = None,
) -> tuple[torch.Tensor, torch.Tensor] | None:
    """Return unary rules as (results, inputs) index tensors, or None."""
    if not self.unary_rules:
        return None

    results, inputs = zip(*self.unary_rules)

    return (
        torch.tensor(results, dtype=torch.long, device=device),
        torch.tensor(inputs, dtype=torch.long, device=device),
    )

unary_weight_tensor

unary_weight_tensor(device: device | None = None) -> Tensor

Return initial unary rule weights as a float tensor.

Returns zeros when no explicit weights are set.

Source code in src/quivers/stochastic/_rule_system.py
def unary_weight_tensor(
    self,
    device: torch.device | None = None,
) -> torch.Tensor:
    """Return initial unary rule weights as a float tensor.

    Returns zeros when no explicit weights are set.
    """
    if self.unary_weights is not None:
        return torch.tensor(
            self.unary_weights,
            dtype=torch.float,
            device=device,
        )

    return torch.zeros(self.n_unary, dtype=torch.float, device=device)

__add__

__add__(other: RuleSystem) -> RuleSystem

Merge two rule systems (deduplicated union).

Source code in src/quivers/stochastic/_rule_system.py
def __add__(self, other: "RuleSystem") -> "RuleSystem":
    """Merge two rule systems (deduplicated union)."""
    if self.n_categories != other.n_categories:
        raise ValueError(
            f"cannot merge rule systems with different category "
            f"counts: {self.n_categories} vs {other.n_categories}"
        )

    seen_binary: dict[tuple[int, ...], float] = {}
    for i, rule in enumerate(self.binary_rules):
        w = self.binary_weights[i] if self.binary_weights else 0.0
        seen_binary[rule] = w
    for i, rule in enumerate(other.binary_rules):
        if rule not in seen_binary:
            w = other.binary_weights[i] if other.binary_weights else 0.0
            seen_binary[rule] = w

    seen_unary: dict[tuple[int, ...], float] = {}
    for i, rule in enumerate(self.unary_rules):
        w = self.unary_weights[i] if self.unary_weights else 0.0
        seen_unary[rule] = w
    for i, rule in enumerate(other.unary_rules):
        if rule not in seen_unary:
            w = other.unary_weights[i] if other.unary_weights else 0.0
            seen_unary[rule] = w

    binary_rules = tuple(seen_binary.keys())
    unary_rules = tuple(seen_unary.keys())

    has_binary_w = (
        self.binary_weights is not None or other.binary_weights is not None
    )
    has_unary_w = self.unary_weights is not None or other.unary_weights is not None

    binary_weights = tuple(seen_binary.values()) if has_binary_w else None
    unary_weights = tuple(seen_unary.values()) if has_unary_w else None

    desc_parts = []
    if self.description:
        desc_parts.append(self.description)
    if other.description:
        desc_parts.append(other.description)

    return RuleSystem(
        binary_rules=binary_rules,
        unary_rules=unary_rules,
        n_categories=self.n_categories,
        description=" + ".join(desc_parts) if desc_parts else "",
        binary_weights=binary_weights,
        unary_weights=unary_weights,
    )

rules

Composable rule systems for chart parsing.

A RuleSystem encapsulates the structural inference rules that govern a grammar -- binary combination rules and unary type-change rules -- as index tensors suitable for CKY chart parsing. Rule systems are the compositional unit: they can be merged (+), inspected, and passed to any ChartParser.

The primitive rule schemas live in quivers.stochastic.schema, which provides composable functors CategorySystem -> RuleSystem. The convenience functions here (ccg_rules, lambek_rules) instantiate schema presets over a given category system.

Categorical perspective

A rule system is a presentation of the arrows in a free category generated by a set of basic combinators. Merging two rule systems takes the coproduct of their generating sets.

ccg_rules

ccg_rules(system: CategorySystem, *, enable_composition: bool = True, enable_crossed_composition: bool = True, enable_type_raising: bool = False, generalized_composition_depth: int = 1) -> RuleSystem

Build a CCG rule system from a category inventory.

PARAMETER DESCRIPTION
system

The finite category inventory.

TYPE: CategorySystem

enable_composition

Enable harmonic composition (>B, <B).

TYPE: bool DEFAULT: True

enable_crossed_composition

Enable crossed composition (>Bx, <Bx).

TYPE: bool DEFAULT: True

enable_type_raising

Enable type raising (>T, <T).

TYPE: bool DEFAULT: False

generalized_composition_depth

Maximum depth for generalized composition (B^n).

TYPE: int DEFAULT: 1

RETURNS DESCRIPTION
RuleSystem

The CCG rule system.

Source code in src/quivers/stochastic/rules.py
def ccg_rules(
    system: CategorySystem,
    *,
    enable_composition: bool = True,
    enable_crossed_composition: bool = True,
    enable_type_raising: bool = False,
    generalized_composition_depth: int = 1,
) -> RuleSystem:
    """Build a CCG rule system from a category inventory.

    Parameters
    ----------
    system : CategorySystem
        The finite category inventory.
    enable_composition : bool
        Enable harmonic composition (>B, <B).
    enable_crossed_composition : bool
        Enable crossed composition (>Bx, <Bx).
    enable_type_raising : bool
        Enable type raising (>T, <T).
    generalized_composition_depth : int
        Maximum depth for generalized composition (B^n).

    Returns
    -------
    RuleSystem
        The CCG rule system.
    """
    schema = EVALUATION

    if enable_composition:
        schema = schema | HARMONIC_COMPOSITION

    if enable_crossed_composition:
        schema = schema | CROSSED_COMPOSITION

    if enable_type_raising:
        schema = schema | ADJUNCTION_UNITS

    if generalized_composition_depth > 1 and enable_composition:
        schema = schema | GeneralizedComposition(
            max_depth=generalized_composition_depth,
        )

    rs = schema(system)

    return RuleSystem(
        binary_rules=rs.binary_rules,
        unary_rules=rs.unary_rules,
        n_categories=rs.n_categories,
        description="CCG",
    )

lambek_rules

lambek_rules(system: CategorySystem, *, associative: bool = True, commutative: bool = False, enable_lifting: bool = True, enable_product: bool = True) -> RuleSystem

Build a Lambek calculus rule system from a category inventory.

PARAMETER DESCRIPTION
system

The finite category inventory.

TYPE: CategorySystem

associative

Use associative Lambek calculus L (default True).

TYPE: bool DEFAULT: True

commutative

Use commutative product / LP calculus (default False).

TYPE: bool DEFAULT: False

enable_lifting

Enable Lambek lifting rules (adjunction units).

TYPE: bool DEFAULT: True

enable_product

Enable product introduction / projection.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
RuleSystem

The Lambek calculus rule system.

Source code in src/quivers/stochastic/rules.py
def lambek_rules(
    system: CategorySystem,
    *,
    associative: bool = True,
    commutative: bool = False,
    enable_lifting: bool = True,
    enable_product: bool = True,
) -> RuleSystem:
    """Build a Lambek calculus rule system from a category inventory.

    Parameters
    ----------
    system : CategorySystem
        The finite category inventory.
    associative : bool
        Use associative Lambek calculus L (default True).
    commutative : bool
        Use commutative product / LP calculus (default False).
    enable_lifting : bool
        Enable Lambek lifting rules (adjunction units).
    enable_product : bool
        Enable product introduction / projection.

    Returns
    -------
    RuleSystem
        The Lambek calculus rule system.
    """
    schema = EVALUATION

    if commutative:
        schema = schema | COMMUTATIVE_EVALUATION

    if enable_lifting:
        schema = schema | ADJUNCTION_UNITS

    if enable_product:
        schema = schema | TENSOR_INTRODUCTION | TENSOR_PROJECTION

    rs = schema(system)

    variant = "L"

    if not associative:
        variant = "NL"

    if commutative:
        variant = "LP"

    return RuleSystem(
        binary_rules=rs.binary_rules,
        unary_rules=rs.unary_rules,
        n_categories=rs.n_categories,
        description=f"Lambek({variant})",
    )

custom_rules

custom_rules(binary: list[tuple[int, int, int]] | None = None, unary: list[tuple[int, int]] | None = None, n_categories: int = 0, description: str = 'custom') -> RuleSystem

Build a rule system from explicit rule triples.

PARAMETER DESCRIPTION
binary

Binary rules as (result, left, right) triples.

TYPE: list of (int, int, int) or None DEFAULT: None

unary

Unary rules as (result, input) pairs.

TYPE: list of (int, int) or None DEFAULT: None

n_categories

Total number of categories.

TYPE: int DEFAULT: 0

description

Label for the rule system.

TYPE: str DEFAULT: 'custom'

RETURNS DESCRIPTION
RuleSystem

The custom rule system.

Source code in src/quivers/stochastic/rules.py
def custom_rules(
    binary: list[tuple[int, int, int]] | None = None,
    unary: list[tuple[int, int]] | None = None,
    n_categories: int = 0,
    description: str = "custom",
) -> RuleSystem:
    """Build a rule system from explicit rule triples.

    Parameters
    ----------
    binary : list of (int, int, int) or None
        Binary rules as (result, left, right) triples.
    unary : list of (int, int) or None
        Unary rules as (result, input) pairs.
    n_categories : int
        Total number of categories.
    description : str
        Label for the rule system.

    Returns
    -------
    RuleSystem
        The custom rule system.
    """
    return RuleSystem(
        binary_rules=tuple(binary or []),
        unary_rules=tuple(unary or []),
        n_categories=n_categories,
        description=description,
    )