Skip to content

Commit e61fa1d

Browse files
committed
Top-k-proofs learning.
1 parent 87ccd02 commit e61fa1d

3 files changed

Lines changed: 155 additions & 39 deletions

File tree

compiler/neural_logica.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1915,11 +1915,23 @@ def LoopRepetitions(self, origins):
19151915
return max(counts) if counts else 1000
19161916

19171917
def RegisterTkpMember(self, predicate):
1918-
"""Compiles a TKP member via the facade and registers it."""
1918+
"""Compiles a TKP member via the facade and registers it.
1919+
1920+
A TKP member declares its own cone inputs: the atom's body
1921+
relation carries the fact axis, and the structural guards of a
1922+
disjunction — theta-independent by contract — never enter the
1923+
cone by dependency, so nobody else would materialize them."""
19191924
member = self.tkp.CompileMember(self, predicate)
19201925
self.members.append(member)
19211926
self.relations[predicate] = Relation(
19221927
predicate, member.key_fields, member.key_types, True)
1928+
if isinstance(member, tkp_logica.TkpAtomMember):
1929+
self.RelationOf(member.atom.body_relation, predicate)
1930+
else:
1931+
for unused_vars, unused_tree, guards, unused_rule in (
1932+
member.tkp_contributions):
1933+
for guard_name, unused_guard_vars in guards:
1934+
self.RelationOf(guard_name, predicate)
19231935
return member
19241936

19251937
def RegisterTkpProbability(self, member):
@@ -2019,6 +2031,7 @@ def RunTkpGroup(content, state, environment, stage, validate):
20192031
seams validate at call time."""
20202032
solver = tkp_logica.SolverOf(runtime)
20212033
solver.EnsureUnit(content.members, content.repetitions)
2034+
solver.CaptureGuards(state, environment)
20222035
if validate:
20232036
theta = tkp_logica.ConcreteTheta(
20242037
solver.ThetaTensor(state, environment))

compiler/tkp_logica.py

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def __init__(self, normalize=None):
210210
self.predicates = {} # name -> k of a proof-valued predicate
211211
self.atoms = {} # name -> TkpAtom
212212
self.probability_predicates = set()
213-
self.disjunction_rules = [] # (name, rule, expression, reads)
213+
self.disjunction_rules = [] # (name, rule, expr, reads, guards)
214214
self.probability_reads = [] # (owner rule, TkpProbability call)
215215

216216
def IsTkp(self, name):
@@ -409,16 +409,20 @@ def Contributions(name, rule):
409409
return [(rule, argument)]
410410
return [(rule, None)]
411411

412-
def CheckContributionBody(contribution_rule, read_names, name):
413-
"""Refuses guards in a contribution body.
414-
415-
The runtime compiles the VALUE EXPRESSION of a contribution only:
416-
proof reads join by their variables and nothing else executes, so
417-
a comparison, an unrelated predicate or a computation in the body
418-
would be silently dropped, changing the meaning of the program.
419-
Allowed: the aggregation machinery (_MultBodyAggAux), the
420-
logica_value binding, and body restatements of the very reads of
421-
the value expression."""
412+
def ContributionGuards(contribution_rule, read_names, name):
413+
"""Collects the RELATIONAL GUARDS of a contribution body.
414+
415+
The first slice of the constraints layer: a body conjunct that is
416+
a plain relation over contribution variables filters the keys of
417+
the contribution — `Reach(a, c) TKP= Step(a, s, c) :-
418+
StartCell(a, s)` seeds reachability from the start only. The
419+
guard relation must be materialized and structural (independent
420+
of the learned parameters: the jax seam captures its rows at
421+
probe time). Everything else in a body stays a loud error:
422+
comparisons, computations and unifications would need value-level
423+
masks — the rest of the constraints design — and silently
424+
dropping them would change the meaning of the program."""
425+
guards = []
422426
conjuncts = (contribution_rule.get('body') or {}).get(
423427
'conjunction', {}).get('conjunct', [])
424428
for conjunct in conjuncts:
@@ -428,20 +432,32 @@ def CheckContributionBody(contribution_rule, read_names, name):
428432
continue
429433
if world.normalize(predicate) in read_names:
430434
continue
431-
Error('TKP: contribution of %s reads %s in its body; the '
432-
'runtime executes only the value expression, so a body '
433-
'guard would be silently ignored. Move the filter into '
434-
'a separate predicate feeding the proof reads.' % (
435-
color.Warn(name), color.Warn(predicate)), name)
435+
if world.IsTkp(predicate):
436+
Error('TKP: contribution of %s reads the proof-valued %s '
437+
'in its body; proof reads belong in the value '
438+
'expression.' % (color.Warn(name),
439+
color.Warn(predicate)), name)
440+
fields = CallFields(conjunct['predicate'])
441+
variables = []
442+
for field in sorted(fields, key=FieldOrder):
443+
variable = VariableName(fields[field])
444+
if variable is None:
445+
Error('TKP: the guard %s of %s must hold plain '
446+
'variables.' % (color.Warn(predicate),
447+
color.Warn(name)), name)
448+
variables.append(variable)
449+
guards.append((predicate, variables))
450+
continue
436451
unification = conjunct.get('unification')
437452
if (unification is not None and VariableName(
438453
unification['left_hand_side']) == 'logica_value'):
439454
continue
440-
Error('TKP: contribution of %s has a guard or computation in '
441-
'its body; the runtime executes only the value '
442-
'expression, so it would be silently ignored. Move the '
443-
'filter into a separate predicate feeding the proof '
444-
'reads.' % color.Warn(name), name)
455+
Error('TKP: contribution of %s has a comparison or computation '
456+
'in its body; only relational guards over contribution '
457+
'variables are supported. Move the rest into a separate '
458+
'predicate feeding the proof reads.' % color.Warn(name),
459+
name)
460+
return guards
445461

446462
for name, rule in rules:
447463
operator, argument = AggregationOperator(rule)
@@ -460,7 +476,7 @@ def CheckContributionBody(contribution_rule, read_names, name):
460476
if contributions and contributions[0][0] is not rule:
461477
# Multi-body form: contributions live in aux rules; the outer
462478
# rule itself must carry nothing beyond the machinery.
463-
CheckContributionBody(rule, set(), name)
479+
ContributionGuards(rule, set(), name)
464480
for contribution_rule, expression in contributions:
465481
reads = []
466482
read_names = set()
@@ -469,12 +485,25 @@ def CheckContributionBody(contribution_rule, read_names, name):
469485
for kind, read in reads:
470486
if kind == 'read':
471487
read_names.add(world.normalize(CallName(read)))
472-
CheckContributionBody(contribution_rule, read_names, name)
488+
guards = ContributionGuards(contribution_rule, read_names,
489+
name)
473490
if expression is None:
474491
Error('TKP: cannot find the contribution of %s.' %
475492
color.Warn(name), name)
493+
known = set(ContributionHeadVariables(contribution_rule, name))
494+
for kind, read in reads:
495+
if kind == 'read':
496+
known.update(CallVariables(read, name))
497+
for guard_name, guard_vars in guards:
498+
for variable in guard_vars:
499+
if variable not in known:
500+
Error('TKP: the guard %s of %s binds %s, which is not a '
501+
'contribution variable; a guard only filters '
502+
'existing keys.' % (
503+
color.Warn(guard_name), color.Warn(name),
504+
color.Warn(variable)), name)
476505
world.disjunction_rules.append(
477-
(name, contribution_rule, expression, reads))
506+
(name, contribution_rule, expression, reads, guards))
478507

479508
# 3. Probability reads: TkpProbability(...) in any expression.
480509
def FindProbabilityReads(node, owner):
@@ -658,7 +687,7 @@ def __init__(self, name, k, contributions):
658687
super().__init__(name)
659688
self.k = k
660689
self.slots = k
661-
self.tkp_contributions = contributions # (head_vars, tree, rule)
690+
self.tkp_contributions = contributions # (vars, tree, guards, rule)
662691

663692

664693
class TkpProbabilityMember(TkpMemberBase):
@@ -842,18 +871,18 @@ def CompileMember(self, plan, predicate):
842871
diamond = predicate + self.diamond_suffix
843872
source = (diamond
844873
if any(name == diamond for name, unused_rule,
845-
unused_expression, unused_reads
874+
unused_expression, unused_reads, unused_guards
846875
in self.world.disjunction_rules)
847876
else predicate)
848877
contributions = []
849-
for name, rule, expression, unused_reads in (
878+
for name, rule, expression, unused_reads, guards in (
850879
self.world.disjunction_rules):
851880
if name != source:
852881
continue
853882
head_vars = ContributionHeadVariables(rule, predicate)
854883
tree = BuildContributionTree(expression, self.world,
855884
self.definitions, predicate)
856-
contributions.append((head_vars, tree, rule))
885+
contributions.append((head_vars, tree, guards, rule))
857886
member = TkpDisjunctionMember(
858887
predicate, self.world.predicates[predicate], contributions)
859888
member.key_fields, member.key_types = self.Signature(plan, predicate)
@@ -947,6 +976,8 @@ def __init__(self, runtime):
947976
self.domains = runtime.domains
948977
self.units = [] # (compiled members, repetitions, unit names)
949978
self.unit_names = set()
979+
self.needed_guards = set()
980+
self.guard_rows = {} # guard relation -> set of key tuples
950981
self.static_masks = {}
951982
self.cache = {}
952983
self.cache_order = []
@@ -957,11 +988,41 @@ def EnsureUnit(self, members, repetitions):
957988
if names in self.unit_names:
958989
return
959990
self.unit_names.add(names)
960-
compiled = [(member, [(head_vars, tree) for head_vars, tree,
961-
unused_rule in member.tkp_contributions])
991+
compiled = [(member,
992+
[(head_vars, tree, guards)
993+
for head_vars, tree, guards, unused_rule
994+
in member.tkp_contributions])
962995
for member in members]
996+
for unused_member, trees in compiled:
997+
for unused_vars, unused_tree, guards in trees:
998+
self.needed_guards.update(
999+
guard_name for guard_name, unused_guard_vars in guards)
9631000
self.units.append((compiled, repetitions, ', '.join(names)))
9641001

1002+
def CaptureGuards(self, state, tensors):
1003+
"""Rows of the guard relations, captured when concrete.
1004+
1005+
Guards are structural — independent of the learned parameters —
1006+
so the probe's concrete capture serves every later (possibly
1007+
traced) call. A guard whose relation never turns up concrete is a
1008+
loud error at solving time."""
1009+
onp = self.onp
1010+
for name in sorted(self.needed_guards - set(self.guard_rows)):
1011+
if name in state:
1012+
mask = state[name][0]
1013+
else:
1014+
try:
1015+
mask = tensors[name][0]
1016+
except Exception:
1017+
continue
1018+
try:
1019+
mask = onp.asarray(mask)
1020+
except Exception:
1021+
continue # Traced; a concrete caller captures it first.
1022+
self.guard_rows[name] = set(
1023+
tuple(int(i) for i in key)
1024+
for key in zip(*onp.nonzero(mask)))
1025+
9651026
def ThetaTensor(self, state, tensors):
9661027
"""The fact probabilities: a differentiable gather at the rows."""
9671028
if self.probability_predicate in state:
@@ -1028,8 +1089,19 @@ def Fingerprint(self, compiled, local):
10281089

10291090
def EvalMember(self, member, trees, view, theta):
10301091
pools = {}
1031-
for head_vars, tree in trees:
1092+
for head_vars, tree, guards in trees:
1093+
filters = []
1094+
for guard_name, guard_vars in guards:
1095+
rows = self.guard_rows.get(guard_name)
1096+
if rows is None:
1097+
Error('TKP: the guard %s never materialized before '
1098+
'solving; a guard must be a structural relation.' %
1099+
color.Warn(guard_name), member.name)
1100+
filters.append((guard_vars, rows))
10321101
for assignment, proofs in self.EvalTree(tree, view, theta):
1102+
if any(tuple(assignment[v] for v in guard_vars) not in rows
1103+
for guard_vars, rows in filters):
1104+
continue
10331105
key = tuple(assignment[v] for v in head_vars)
10341106
pools.setdefault(key, []).extend(proofs)
10351107
return {key: SparseCanonicalize(pool, theta, member.k)
@@ -1140,6 +1212,7 @@ def EvaluateDisjunction(state, tensors):
11401212
# stages run this function (recursion groups register through
11411213
# their own stage with their declared bound).
11421214
solver.EnsureUnit([member], 2)
1215+
solver.CaptureGuards(state, tensors)
11431216
theta_tensor = solver.ThetaTensor(state, tensors)
11441217
theta = ConcreteTheta(theta_tensor)
11451218
return (solver.StaticMask(member, theta), None)
@@ -1178,6 +1251,7 @@ def BackwardArray(cotangent, theta_values):
11781251
if hasattr(jnp, 'custom'): # The logix seam.
11791252

11801253
def EvaluateLogix(state, tensors):
1254+
solver.CaptureGuards(state, tensors)
11811255
theta_tensor = solver.ThetaTensor(state, tensors)
11821256
mask = solver.StaticMask(member, ConcreteTheta(theta_tensor))
11831257
values = jnp.custom(
@@ -1210,6 +1284,7 @@ def PublishedBackward(theta_tensor, cotangent):
12101284
Published.defvjp(PublishedForward, PublishedBackward)
12111285

12121286
def EvaluateJax(state, tensors):
1287+
solver.CaptureGuards(state, tensors)
12131288
theta_tensor = solver.ThetaTensor(state, tensors)
12141289
mask = solver.StaticMask(member, ConcreteTheta(theta_tensor))
12151290
return mask, Published(theta_tensor)

examples/extensions/tkp_tests/tkp_tensor_test.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,13 @@ def _ExpectCompileError(program, fragment):
328328

329329

330330
def TestGuardsRefused():
331-
"""Guards outside the structural skeleton are a loud compile error.
331+
"""Non-relational guards stay a loud compile error.
332332
333-
The runtime materializes ALL rows of an atom's body relation and
334-
executes ONLY the value expression of a contribution; before the
335-
checks `... :- Raw(a, b), a != b` kept the self-edges and
336-
`Path(x, z) TKP= ... :- x != z` dropped the filter — both silently
337-
changing the meaning of the program."""
333+
The runtime materializes ALL rows of an atom's body relation, and a
334+
contribution body admits only RELATIONAL guards over contribution
335+
variables (the first slice of the constraints layer); comparisons
336+
and computations would need value-level masks and are refused —
337+
silently dropping them once changed the meaning of the program."""
338338
atom = ('Edge(a, b) = TkpMakeFact(predicate: "e", args: [a, b], '
339339
'probability: P(a, b)) :- Raw(a, b)%s;')
340340
_ExpectCompileError(atom % ', a != b', 'guard or computation')
@@ -343,7 +343,34 @@ def TestGuardsRefused():
343343
'T2P(x) = TkpTop(MergeList(x), 2); '
344344
'Path(x, z) T2P= TkpProbConjunction(Path(x, y), Path(y, z)) '
345345
':- x != z;')
346-
_ExpectCompileError(disjunction, 'guard or computation')
346+
_ExpectCompileError(disjunction, 'comparison or computation')
347+
return 0
348+
349+
350+
def TestRelationalGuardCollected():
351+
"""A relational guard over contribution variables is collected.
352+
353+
`Reach(c) TKP= Step(s, c) :- Start(s)` — the base of reachability
354+
seeded from the start only; a guard binding a foreign variable is
355+
refused (it could only filter, never bind)."""
356+
from parser_py import parse
357+
program = (
358+
'T2P(x) = TkpTop(MergeList(x), 2); '
359+
'Reach(c) T2P= Step(s, c) :- Start(s); '
360+
'Step(s, c) = TkpMakeFact(predicate: "s", args: [s, c], '
361+
'probability: P(s, c)) :- Raw(s, c);')
362+
rules = parse.ParseFile(program)['rule']
363+
pairs = [(rule['head']['predicate_name'], rule) for rule in rules]
364+
world = tkp_logica.ExtractTkpWorld(pairs)
365+
guard_lists = [guards for name, rule, expression, reads, guards
366+
in world.disjunction_rules if name == 'Reach']
367+
assert guard_lists == [[('Start', ['s'])]], guard_lists
368+
_ExpectCompileError(
369+
'T2P(x) = TkpTop(MergeList(x), 2); '
370+
'Reach(c) T2P= Step(s, c) :- Alien(q); '
371+
'Step(s, c) = TkpMakeFact(predicate: "s", args: [s, c], '
372+
'probability: P(s, c)) :- Raw(s, c);',
373+
'not a contribution variable')
347374
return 0
348375

349376

@@ -476,6 +503,7 @@ def main():
476503
failures += TestThetaDependentHorizon()
477504
failures += TestSparseScale()
478505
failures += TestGuardsRefused()
506+
failures += TestRelationalGuardCollected()
479507
failures += TestRepeatedVariablesRefused()
480508
failures += TestNonPositiveKRefused()
481509
failures += TestCanonicalFieldOrder()

0 commit comments

Comments
 (0)