Rule Systems

Composable structural rule systems for chart parsing. A RuleSystem encapsulates binary and unary inference rules as index tensors and supports merging (+) for hybrid grammars.

Each rule system optionally carries learnable weights, one log-weight per binary rule and one per unary rule. These weights are added to the chart combination scores during parsing. When a RuleSystem is passed to a ChartParser, its weights initialize the parser's nn.Parameter tensors (or fixed buffers if learnable_rule_weights=False). Weight-preserving merge: when two rule systems are combined with +, weights from both systems are preserved for their respective rules; 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 |.

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.

RuleSystem

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
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
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,
    )

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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
104
105
106
107
108
109
110
111
112
113
114
115
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
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
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
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,
    )