eyeling 1.5.41 → 1.5.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ # ================================================================
2
+ # Heavy-Math N3 Demo: "Saffron Slopeworks"
3
+ # ------------------------------------------------
4
+ # A math-stress N3 ruleset focused on *linear regression* and
5
+ # residual analysis, intended for testing strict W3C/N3 math
6
+ # builtins implementations.
7
+ #
8
+ # What it does:
9
+ # 1) Computes least-squares line: y = a + b x
10
+ # - slope b, intercept a
11
+ # 2) Computes Pearson correlation r and r^2
12
+ # 3) Computes SSE + RMSE
13
+ # 4) Flags points whose |residual| > 2*RMSE (as high-residual outliers)
14
+ # 5) Predicts y at a given x
15
+ #
16
+ # Notes:
17
+ # - Uses only: math:, list:, log:
18
+ # - Head blank nodes are used to create connected result structures.
19
+ # ================================================================
20
+
21
+ @prefix : <http://example.org/saffron-slopeworks#> .
22
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
23
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
24
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
25
+
26
+ ############
27
+ # DATA
28
+ ############
29
+
30
+ :Reg1
31
+ :points (
32
+ [ :x 1.0 ; :y 2.1 ]
33
+ [ :x 2.0 ; :y 2.9 ]
34
+ [ :x 3.0 ; :y 3.8 ]
35
+ [ :x 4.0 ; :y 4.2 ]
36
+ [ :x 5.0 ; :y 5.1 ]
37
+ [ :x 6.0 ; :y 5.9 ]
38
+ [ :x 7.0 ; :y 7.0 ]
39
+ [ :x 8.0 ; :y 15.0 ] # an obvious high-residual outlier
40
+ ) ;
41
+ :predictX 8.5 .
42
+
43
+ ############
44
+ # (1) COLLECT SUMS: Σx, Σy, Σx², Σy², Σxy
45
+ ############
46
+
47
+ {
48
+ :Reg1 :points ?pts .
49
+ ?pts list:length ?n .
50
+
51
+ ( ?x { ?pts list:member ?p . ?p :x ?x . } ?xs ) log:collectAllIn _:s1 .
52
+ ( ?y { ?pts list:member ?p . ?p :y ?y . } ?ys ) log:collectAllIn _:s1 .
53
+
54
+ ?xs math:sum ?sumX .
55
+ ?ys math:sum ?sumY .
56
+
57
+ ( ?x2 {
58
+ ?pts list:member ?p .
59
+ ?p :x ?x .
60
+ (?x 2.0) math:exponentiation ?x2 .
61
+ } ?x2s ) log:collectAllIn _:s1 .
62
+ ?x2s math:sum ?sumXX .
63
+
64
+ ( ?y2 {
65
+ ?pts list:member ?p .
66
+ ?p :y ?y .
67
+ (?y 2.0) math:exponentiation ?y2 .
68
+ } ?y2s ) log:collectAllIn _:s1 .
69
+ ?y2s math:sum ?sumYY .
70
+
71
+ ( ?xy {
72
+ ?pts list:member ?p .
73
+ ?p :x ?x ; :y ?y .
74
+ (?x ?y) math:product ?xy .
75
+ } ?xys ) log:collectAllIn _:s1 .
76
+ ?xys math:sum ?sumXY .
77
+ }
78
+ =>
79
+ {
80
+ :Reg1 :n ?n ;
81
+ :sumX ?sumX ; :sumY ?sumY ;
82
+ :sumXX ?sumXX ; :sumYY ?sumYY ;
83
+ :sumXY ?sumXY .
84
+ } .
85
+
86
+ ############
87
+ # (2) REGRESSION + CORRELATION
88
+ #
89
+ # b = ( n*Σxy - (Σx)(Σy) ) / ( n*Σx² - (Σx)² )
90
+ # a = ( Σy - b*Σx ) / n
91
+ #
92
+ # r = ( n*Σxy - (Σx)(Σy) ) / sqrt( (n*Σx²-(Σx)²)(n*Σy²-(Σy)²) )
93
+ # r² = r^2
94
+ ############
95
+
96
+ {
97
+ :Reg1 :n ?n ;
98
+ :sumX ?sx ; :sumY ?sy ;
99
+ :sumXX ?sxx ; :sumYY ?syy ;
100
+ :sumXY ?sxy .
101
+
102
+ # numerator = n*sxy - sx*sy
103
+ (?n ?sxy) math:product ?n_sxy .
104
+ (?sx ?sy) math:product ?sx_sy .
105
+ (?n_sxy ?sx_sy) math:difference ?num .
106
+
107
+ # denX = n*sxx - sx^2
108
+ (?n ?sxx) math:product ?n_sxx .
109
+ (?sx 2.0) math:exponentiation ?sx2 .
110
+ (?n_sxx ?sx2) math:difference ?denX .
111
+
112
+ # slope b
113
+ (?num ?denX) math:quotient ?b .
114
+
115
+ # intercept a = (sy - b*sx) / n
116
+ (?b ?sx) math:product ?b_sx .
117
+ (?sy ?b_sx) math:difference ?tmpA .
118
+ (?tmpA ?n) math:quotient ?a .
119
+
120
+ # denY = n*syy - sy^2
121
+ (?n ?syy) math:product ?n_syy .
122
+ (?sy 2.0) math:exponentiation ?sy2 .
123
+ (?n_syy ?sy2) math:difference ?denY .
124
+
125
+ # r = num / sqrt(denX*denY)
126
+ (?denX ?denY) math:product ?denXY .
127
+ (?denXY 0.5) math:exponentiation ?sqrtDen .
128
+ (?num ?sqrtDen) math:quotient ?r .
129
+ (?r 2.0) math:exponentiation ?r2 .
130
+ }
131
+ =>
132
+ {
133
+ :Reg1 :slope ?b ;
134
+ :intercept ?a ;
135
+ :pearsonR ?r ;
136
+ :rSquared ?r2 .
137
+ } .
138
+
139
+ ############
140
+ # (3) SSE + RMSE (root mean squared error)
141
+ # residual e = y - (a + b x)
142
+ # SSE = Σ e^2
143
+ # RMSE = sqrt(SSE / n)
144
+ ############
145
+
146
+ {
147
+ :Reg1 :points ?pts ; :slope ?b ; :intercept ?a .
148
+ ?pts list:length ?n .
149
+
150
+ ( ?e2 {
151
+ ?pts list:member ?p .
152
+ ?p :x ?x ; :y ?y .
153
+
154
+ (?b ?x) math:product ?bx .
155
+ (?a ?bx) math:sum ?yhat .
156
+ (?y ?yhat) math:difference ?e .
157
+ (?e 2.0) math:exponentiation ?e2 .
158
+ } ?e2s ) log:collectAllIn _:s2 .
159
+
160
+ ?e2s math:sum ?sse .
161
+ (?sse ?n) math:quotient ?mse .
162
+ (?mse 0.5) math:exponentiation ?rmse .
163
+ }
164
+ =>
165
+ {
166
+ :Reg1 :sse ?sse ;
167
+ :rmse ?rmse .
168
+ } .
169
+
170
+ ############
171
+ # (4) FLAG HIGH-RESIDUAL POINTS: |e| > 2*RMSE
172
+ ############
173
+
174
+ {
175
+ :Reg1 :points ?pts ; :slope ?b ; :intercept ?a ; :rmse ?rmse .
176
+ ?pts list:member ?p .
177
+ ?p :x ?x ; :y ?y .
178
+
179
+ (?b ?x) math:product ?bx .
180
+ (?a ?bx) math:sum ?yhat .
181
+ (?y ?yhat) math:difference ?e .
182
+ ?e math:absoluteValue ?ae .
183
+
184
+ (2.0 ?rmse) math:product ?thr .
185
+ ?ae math:greaterThan ?thr .
186
+ }
187
+ =>
188
+ {
189
+ :Reg1 :highResidual
190
+ [ :point ?p ;
191
+ :x ?x ; :y ?y ;
192
+ :yhat ?yhat ;
193
+ :residual ?e ] .
194
+ } .
195
+
196
+ ############
197
+ # (5) PREDICTION: y = a + b*x0
198
+ ############
199
+
200
+ {
201
+ :Reg1 :predictX ?x0 ; :slope ?b ; :intercept ?a .
202
+ (?b ?x0) math:product ?bx0 .
203
+ (?a ?bx0) math:sum ?y0 .
204
+ }
205
+ =>
206
+ {
207
+ :Reg1 :prediction [ :x ?x0 ; :y ?y0 ] .
208
+ } .
209
+
210
+ ############
211
+ # STRICTNESS REGRESSION HOOK (should fail in strict math)
212
+ ############
213
+ # {
214
+ # "2"^^<http://example.org/not-a-number> math:product "3"^^<http://example.org/nope> .
215
+ # } => { :bad :datatype :accepted . } .
216
+
package/eyeling.js CHANGED
@@ -1169,44 +1169,70 @@ function liftBlankRuleVars(premise, conclusion) {
1169
1169
  return [newPremise, conclusion];
1170
1170
  }
1171
1171
 
1172
- function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter) {
1172
+ // Skolemization for blank nodes that occur explicitly in a rule head.
1173
+ //
1174
+ // IMPORTANT: we must be *stable per rule firing*, otherwise a rule whose
1175
+ // premises stay true would keep generating fresh _:sk_N blank nodes on every
1176
+ // outer fixpoint iteration (non-termination once we do strict duplicate checks).
1177
+ //
1178
+ // We achieve this by optionally keying head-blank allocations by a "firingKey"
1179
+ // (usually derived from the instantiated premises and rule index) and caching
1180
+ // them in a run-global map.
1181
+ function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
1173
1182
  if (t instanceof Blank) {
1174
1183
  const label = t.label;
1175
1184
  // Only skolemize blanks that occur explicitly in the rule head
1176
1185
  if (!headBlankLabels || !headBlankLabels.has(label)) {
1177
1186
  return t; // this is a data blank (e.g. bound via ?X), keep it
1178
1187
  }
1188
+
1179
1189
  if (!mapping.hasOwnProperty(label)) {
1180
- const idx = skCounter[0];
1181
- skCounter[0] += 1;
1182
- mapping[label] = `_:sk_${idx}`;
1190
+ // If we have a global cache keyed by firingKey, use it to ensure
1191
+ // deterministic blank IDs for the same rule+substitution instance.
1192
+ if (globalMap && firingKey) {
1193
+ const gk = `${firingKey}|${label}`;
1194
+ let sk = globalMap.get(gk);
1195
+ if (!sk) {
1196
+ const idx = skCounter[0];
1197
+ skCounter[0] += 1;
1198
+ sk = `_:sk_${idx}`;
1199
+ globalMap.set(gk, sk);
1200
+ }
1201
+ mapping[label] = sk;
1202
+ } else {
1203
+ const idx = skCounter[0];
1204
+ skCounter[0] += 1;
1205
+ mapping[label] = `_:sk_${idx}`;
1206
+ }
1183
1207
  }
1184
1208
  return new Blank(mapping[label]);
1185
1209
  }
1186
1210
 
1187
1211
  if (t instanceof ListTerm) {
1188
- return new ListTerm(t.elems.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter)));
1212
+ return new ListTerm(t.elems.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)));
1189
1213
  }
1190
1214
 
1191
1215
  if (t instanceof OpenListTerm) {
1192
1216
  return new OpenListTerm(
1193
- t.prefix.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter)),
1217
+ t.prefix.map((e) => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter, firingKey, globalMap)),
1194
1218
  t.tailVar,
1195
1219
  );
1196
1220
  }
1197
1221
 
1198
1222
  if (t instanceof FormulaTerm) {
1199
- return new FormulaTerm(t.triples.map((tr) => skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter)));
1223
+ return new FormulaTerm(
1224
+ t.triples.map((tr) => skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap)),
1225
+ );
1200
1226
  }
1201
1227
 
1202
1228
  return t;
1203
1229
  }
1204
1230
 
1205
- function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter) {
1231
+ function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
1206
1232
  return new Triple(
1207
- skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter),
1208
- skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter),
1209
- skolemizeTermForHeadBlanks(tr.o, headBlankLabels, mapping, skCounter),
1233
+ skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter, firingKey, globalMap),
1234
+ skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter, firingKey, globalMap),
1235
+ skolemizeTermForHeadBlanks(tr.o, headBlankLabels, mapping, skCounter, firingKey, globalMap),
1210
1236
  );
1211
1237
  }
1212
1238
 
@@ -1502,15 +1528,20 @@ function hasFactIndexed(facts, tr) {
1502
1528
  const po = facts.__byPO.get(pk);
1503
1529
  if (po) {
1504
1530
  const pob = po.get(ok) || [];
1505
- return pob.some((t) => alphaEqTriple(t, tr));
1531
+ // Facts are all in the same graph. Different blank node labels represent
1532
+ // different existentials unless explicitly connected. Do NOT treat
1533
+ // triples as duplicates modulo blank renaming, or you'll incorrectly
1534
+ // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
1535
+ return pob.some((t) => triplesEqual(t, tr));
1506
1536
  }
1507
1537
  }
1508
1538
 
1509
1539
  const pb = facts.__byPred.get(pk) || [];
1510
- return pb.some((t) => alphaEqTriple(t, tr));
1540
+ return pb.some((t) => triplesEqual(t, tr));
1511
1541
  }
1512
1542
 
1513
- return hasAlphaEquiv(facts, tr);
1543
+ // Non-IRI predicate: fall back to strict triple equality.
1544
+ return facts.some((t) => triplesEqual(t, tr));
1514
1545
  }
1515
1546
 
1516
1547
  function pushFactIndexed(facts, tr) {
@@ -4162,6 +4193,20 @@ function forwardChain(facts, forwardRules, backRules) {
4162
4193
  const varGen = [0];
4163
4194
  const skCounter = [0];
4164
4195
 
4196
+ // Cache head blank-node skolemization per (rule firing, head blank label).
4197
+ // This prevents repeatedly generating fresh _:sk_N blanks for the *same*
4198
+ // rule+substitution instance across outer fixpoint iterations.
4199
+ const headSkolemCache = new Map();
4200
+
4201
+ function firingKey(ruleIndex, instantiatedPremises) {
4202
+ // Deterministic key derived from the instantiated body (ground per substitution).
4203
+ const parts = [];
4204
+ for (const tr of instantiatedPremises) {
4205
+ parts.push(JSON.stringify([skolemKeyFromTerm(tr.s), skolemKeyFromTerm(tr.p), skolemKeyFromTerm(tr.o)]));
4206
+ }
4207
+ return `R${ruleIndex}|` + parts.join('\\n');
4208
+ }
4209
+
4165
4210
  // Make rules visible to introspection builtins
4166
4211
  backRules.__allForwardRules = forwardRules;
4167
4212
  backRules.__allBackwardRules = backRules;
@@ -4187,6 +4232,7 @@ function forwardChain(facts, forwardRules, backRules) {
4187
4232
  // (e.g., from [ :p ... ; :q ... ]) stay connected across all head triples.
4188
4233
  const skMap = {};
4189
4234
  const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
4235
+ const fireKey = firingKey(i, instantiatedPremises);
4190
4236
 
4191
4237
  for (const cpat of r.conclusion) {
4192
4238
  const instantiated = applySubstTriple(cpat, s);
@@ -4270,7 +4316,7 @@ function forwardChain(facts, forwardRules, backRules) {
4270
4316
  }
4271
4317
 
4272
4318
  // Only skolemize blank nodes that occur explicitly in the rule head
4273
- const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter);
4319
+ const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter, fireKey, headSkolemCache);
4274
4320
 
4275
4321
  if (!isGroundTriple(inst)) continue;
4276
4322
  if (hasFactIndexed(facts, inst)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.5.41",
3
+ "version": "1.5.42",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "test:examples": "node test/examples.test.js",
35
35
  "test:package": "node test/package.test.js",
36
36
  "test": "npm run test:packlist && npm run test:api && npm run test:examples",
37
- "preversion": "prettier -w eyeling.js && npm test",
37
+ "preversion": "npm test",
38
38
  "postversion": "git push origin HEAD --follow-tags"
39
39
  }
40
40
  }