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.
- package/examples/output/saffron-slopeworks.n3 +1470 -0
- package/examples/saffron-slopeworks.n3 +216 -0
- package/eyeling.js +61 -15
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
1540
|
+
return pb.some((t) => triplesEqual(t, tr));
|
|
1511
1541
|
}
|
|
1512
1542
|
|
|
1513
|
-
|
|
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.
|
|
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": "
|
|
37
|
+
"preversion": "npm test",
|
|
38
38
|
"postversion": "git push origin HEAD --follow-tags"
|
|
39
39
|
}
|
|
40
40
|
}
|