eyeling 1.17.2 → 1.18.1

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,257 @@
1
+ # ==================================================
2
+ # sudoku.n3
3
+ #
4
+ # A standalone Sudoku solver and report generator.
5
+ #
6
+ # Edit :case :puzzle to solve another puzzle.
7
+ # Accepted blanks: 0, ., _
8
+ # Whitespace and ASCII board separators are ignored.
9
+ # ==================================================
10
+
11
+ @prefix : <http://example.org/sudoku#> .
12
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
13
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
14
+ @prefix sb: <http://example.org/sudoku-builtin#> .
15
+
16
+ # -----
17
+ # Facts
18
+ # -----
19
+
20
+ :case :name "sudoku" .
21
+ :case :puzzleName "classic" .
22
+ :case :puzzle "100007090030020008009600500005300900010080002600004000300000010040000007007000300" .
23
+
24
+ # ------------------------------------
25
+ # Report facts from the solver builtin
26
+ # ------------------------------------
27
+
28
+ { :case :puzzle ?p .
29
+ ?p sb:status ?status .
30
+ ?p sb:normalizedPuzzle ?normalized .
31
+ ?p sb:givens ?givens .
32
+ ?p sb:blanks ?blanks .
33
+ ?p sb:puzzleText ?puzzleText .
34
+ }
35
+ => {
36
+ :case :status ?status ;
37
+ :normalizedPuzzle ?normalized ;
38
+ :givens ?givens ;
39
+ :blanks ?blanks ;
40
+ :puzzleText ?puzzleText .
41
+ } .
42
+
43
+ { :case :puzzle ?p .
44
+ ?p sb:status "ok" .
45
+ ?p sb:solution ?solution ;
46
+ sb:solutionText ?solutionText ;
47
+ sb:forcedMoves ?forced ;
48
+ sb:guessedMoves ?guessed ;
49
+ sb:recursiveNodes ?nodes ;
50
+ sb:backtracks ?backtracks ;
51
+ sb:maxDepth ?maxDepth ;
52
+ sb:unique ?unique ;
53
+ sb:moveSummary ?moveSummary ;
54
+ sb:givensPreservedText ?givensPreserved ;
55
+ sb:noBlanksText ?noBlanks ;
56
+ sb:rowsCompleteText ?rowsComplete ;
57
+ sb:colsCompleteText ?colsComplete ;
58
+ sb:boxesCompleteText ?boxesComplete ;
59
+ sb:replayLegalText ?replayLegal ;
60
+ sb:storyConsistentText ?storyConsistent .
61
+ }
62
+ => {
63
+ :case :solution ?solution ;
64
+ :solutionText ?solutionText ;
65
+ :forcedMoves ?forced ;
66
+ :guessedMoves ?guessed ;
67
+ :recursiveNodes ?nodes ;
68
+ :backtracks ?backtracks ;
69
+ :maxDepth ?maxDepth ;
70
+ :unique ?unique ;
71
+ :moveSummary ?moveSummary ;
72
+ :givensPreserved ?givensPreserved ;
73
+ :noBlanks ?noBlanks ;
74
+ :rowsComplete ?rowsComplete ;
75
+ :colsComplete ?colsComplete ;
76
+ :boxesComplete ?boxesComplete ;
77
+ :replayLegal ?replayLegal ;
78
+ :storyConsistent ?storyConsistent .
79
+ } .
80
+
81
+ { :case :puzzle ?p .
82
+ ?p sb:status ?status .
83
+ ?status string:notEqualIgnoringCase "ok" .
84
+ ?p sb:error ?error .
85
+ }
86
+ => { :case :error ?error . } .
87
+
88
+ # ------------
89
+ # Presentation
90
+ # ------------
91
+
92
+ # Successful solve, unique.
93
+ { :case :status "ok" .
94
+ :case :unique true .
95
+ :case :givens ?givens ;
96
+ :blanks ?blanks ;
97
+ :forcedMoves ?forced ;
98
+ :guessedMoves ?guessed ;
99
+ :recursiveNodes ?nodes ;
100
+ :backtracks ?backtracks ;
101
+ :moveSummary ?moveSummary ;
102
+ :puzzleText ?puzzleText ;
103
+ :solutionText ?solutionText .
104
+ ( "The puzzle is solved, and the completed grid is the unique valid Sudoku solution.\n" ) string:concatenation ?answerText .
105
+ ( "The solver starts from %s clues and fills the remaining %s cells by combining constraint propagation with depth-first search. At each step it chooses the empty cell with the fewest legal digits, places forced singles immediately, and only guesses when more than one candidate remains. Across the search it made %s forced placements and tried %s guesses, visited %s search nodes overall, and backtracked %s times before reaching the completed grid. The solver also confirmed that the solution is unique. Early steps: %s"
106
+ ?givens ?blanks ?forced ?guessed ?nodes ?backtracks ?moveSummary ) string:format ?reasonText .
107
+ }
108
+ => {
109
+ :01-answer log:outputString "=== Answer ===\n" ;
110
+ log:outputString ?answerText .
111
+ :01a-answer log:outputString "case : sudoku\n" ;
112
+ log:outputString "default puzzle : classic\n\n" ;
113
+ log:outputString "Puzzle\n" ;
114
+ log:outputString ?puzzleText ;
115
+ log:outputString "\n" ;
116
+ log:outputString "Completed grid\n" ;
117
+ log:outputString ?solutionText ;
118
+ log:outputString "\n\n" .
119
+ :02-reason log:outputString "=== Reason Why ===\n" ;
120
+ log:outputString ?reasonText ;
121
+ log:outputString "\n\n" .
122
+ } .
123
+
124
+ # Successful solve, not unique.
125
+ { :case :status "ok" .
126
+ :case :unique false .
127
+ :case :givens ?givens ;
128
+ :blanks ?blanks ;
129
+ :forcedMoves ?forced ;
130
+ :guessedMoves ?guessed ;
131
+ :recursiveNodes ?nodes ;
132
+ :backtracks ?backtracks ;
133
+ :moveSummary ?moveSummary ;
134
+ :puzzleText ?puzzleText ;
135
+ :solutionText ?solutionText .
136
+ ( "The puzzle is solved, and the completed grid is a valid Sudoku solution.\n" ) string:concatenation ?answerText .
137
+ ( "The solver starts from %s clues and fills the remaining %s cells by combining constraint propagation with depth-first search. At each step it chooses the empty cell with the fewest legal digits, places forced singles immediately, and only guesses when more than one candidate remains. Across the search it made %s forced placements and tried %s guesses, visited %s search nodes overall, and backtracked %s times before reaching the completed grid. The solver found a valid solution, but there is more than one. Early steps: %s"
138
+ ?givens ?blanks ?forced ?guessed ?nodes ?backtracks ?moveSummary ) string:format ?reasonText .
139
+ }
140
+ => {
141
+ :01-answer log:outputString "=== Answer ===\n" ;
142
+ log:outputString ?answerText .
143
+ :01a-answer log:outputString "case : sudoku\n" ;
144
+ log:outputString "default puzzle : classic\n\n" ;
145
+ log:outputString "Puzzle\n" ;
146
+ log:outputString ?puzzleText ;
147
+ log:outputString "\n" ;
148
+ log:outputString "Completed grid\n" ;
149
+ log:outputString ?solutionText ;
150
+ log:outputString "\n" .
151
+ :02-reason log:outputString "=== Reason Why ===\n" ;
152
+ log:outputString ?reasonText ;
153
+ log:outputString "\n\n" .
154
+ } .
155
+
156
+ # Invalid input length / characters.
157
+ { :case :status "invalid-input" .
158
+ :case :error ?error .
159
+ }
160
+ => {
161
+ :01-answer log:outputString "=== Answer ===\n" ;
162
+ log:outputString "The supplied puzzle is not well formed and cannot be parsed as a 9×9 Sudoku.\n" ;
163
+ log:outputString "case : sudoku\n\n" .
164
+ :02-reason log:outputString "=== Reason Why ===\n" ;
165
+ log:outputString ?error ;
166
+ log:outputString "\n\n" .
167
+ } .
168
+
169
+ # Illegal clues.
170
+ { :case :status "illegal-clues" .
171
+ :case :puzzleText ?puzzleText ;
172
+ :error ?error .
173
+ }
174
+ => {
175
+ :01-answer log:outputString "=== Answer ===\n" ;
176
+ log:outputString "The puzzle is invalid and cannot be solved as a standard Sudoku.\n" ;
177
+ log:outputString "case : sudoku\n\n" ;
178
+ log:outputString "Puzzle\n" ;
179
+ log:outputString ?puzzleText ;
180
+ log:outputString "\n" .
181
+ :02-reason log:outputString "=== Reason Why ===\n" ;
182
+ log:outputString ?error ;
183
+ log:outputString "\n\n" .
184
+ } .
185
+
186
+ # Unsatisfiable puzzle.
187
+ { :case :status "unsatisfiable" .
188
+ :case :givens ?givens ;
189
+ :blanks ?blanks ;
190
+ :recursiveNodes ?nodes ;
191
+ :backtracks ?backtracks ;
192
+ :puzzleText ?puzzleText .
193
+ ( "The solver explored %s search nodes with minimum-remaining-values branching and backtracked %s times, but every branch eventually contradicted the row, column, or box constraints."
194
+ ?nodes ?backtracks ) string:format ?reasonText .
195
+ }
196
+ => {
197
+ :01-answer log:outputString "=== Answer ===\n" ;
198
+ log:outputString "No valid Sudoku solution exists for the supplied puzzle.\n" ;
199
+ log:outputString "case : sudoku\n" ;
200
+ log:outputString "default puzzle : classic\n" ;
201
+ log:outputString "\nPuzzle\n" ;
202
+ log:outputString ?puzzleText ;
203
+ log:outputString "\n" .
204
+ :02-reason log:outputString "=== Reason Why ===\n" ;
205
+ log:outputString ?reasonText ;
206
+ log:outputString "\n\n" .
207
+ } .
208
+
209
+ # -----------
210
+ # Check block
211
+ # -----------
212
+
213
+ { :case :status "ok" .
214
+ :case :givensPreserved ?c1 ;
215
+ :noBlanks ?c2 ;
216
+ :rowsComplete ?c3 ;
217
+ :colsComplete ?c4 ;
218
+ :boxesComplete ?c5 ;
219
+ :replayLegal ?c6 ;
220
+ :storyConsistent ?c7 .
221
+ ( "C1 %s - every given clue is preserved in the final grid.\n" ?c1 ) string:format ?l1 .
222
+ ( "C2 %s - the final grid contains only digits 1 through 9, with no blanks left.\n" ?c2 ) string:format ?l2 .
223
+ ( "C3 %s - each row contains every digit exactly once.\n" ?c3 ) string:format ?l3 .
224
+ ( "C4 %s - each column contains every digit exactly once.\n" ?c4 ) string:format ?l4 .
225
+ ( "C5 %s - each 3×3 box contains every digit exactly once.\n" ?c5 ) string:format ?l5 .
226
+ ( "C6 %s - replaying the recorded placements from the original puzzle remains legal at every step.\n" ?c6 ) string:format ?l6 .
227
+ ( "C7 %s - the search statistics and the successful proof path are internally consistent.\n" ?c7 ) string:format ?l7 .
228
+ }
229
+ => {
230
+ :03-check log:outputString "=== Check ===\n" ;
231
+ log:outputString ?l1 ;
232
+ log:outputString ?l2 ;
233
+ log:outputString ?l3 ;
234
+ log:outputString ?l4 ;
235
+ log:outputString ?l5 ;
236
+ log:outputString ?l6 ;
237
+ log:outputString ?l7 .
238
+ } .
239
+
240
+ { :case :status "ok" .
241
+ :case :unique true .
242
+ }
243
+ => { :03a-check log:outputString "C8 OK - a second search found no alternative solution, so the solution is unique.\n" . } .
244
+
245
+ { :case :status "ok" .
246
+ :case :unique false .
247
+ }
248
+ => { :03a-check log:outputString "C8 INFO - a second search found another solution, so the puzzle is not unique.\n" . } .
249
+
250
+ { :case :status "illegal-clues" . }
251
+ => { :03-check log:outputString "=== Check ===\nC1 failed - the given clues already violate Sudoku rules.\n" . } .
252
+
253
+ { :case :status "invalid-input" . }
254
+ => { :03-check log:outputString "=== Check ===\nC1 failed - the supplied text does not normalize to exactly 81 legal Sudoku cells.\n" . } .
255
+
256
+ { :case :status "unsatisfiable" . }
257
+ => { :03-check log:outputString "=== Check ===\nC1 OK - the given clues are internally consistent.\nC2 OK - every explored assignment respected row, column, and box legality.\nC3 failed - exhaustive search found no complete legal grid.\n" . } .
@@ -0,0 +1,299 @@
1
+ # =============================================================================
2
+ # transistor-switch.n3
3
+ #
4
+ # - supply voltage = 5000 mV
5
+ # - low input = 0 mV
6
+ # - high input = 5000 mV
7
+ # - base-emitter turn-on = 700 mV
8
+ # - collector-emitter sat = 200 mV
9
+ # - base resistor = 10000 ohms
10
+ # - load resistor = 1000 ohms
11
+ # - transistor beta proxy = 100
12
+ #
13
+ # We model an NPN low-side switch with exact millivolt and microamp arithmetic.
14
+ # If Vin <= Vbe,on the transistor stays in cutoff and the collector branch is
15
+ # OFF. If Vin > Vbe,on then Ib = (Vin - Vbe,on) * 1000 / Rb, the gain-limited
16
+ # collector current is beta * Ib, the load-limited current is
17
+ # (Vcc - Vce,sat) * 1000 / Rl, and the actual collector current is the smaller
18
+ # of those two limits.
19
+ # =============================================================================
20
+
21
+ @prefix : <http://example.org/transistor-switch#> .
22
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
23
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
24
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
25
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
26
+
27
+ # -----
28
+ # Facts
29
+ # -----
30
+
31
+ :case :name "transistor-switch" .
32
+ :case :supplyMv 5000 .
33
+ :case :vbeOnMv 700 .
34
+ :case :vceSatMv 200 .
35
+ :case :baseResistanceOhms 10000 .
36
+ :case :loadResistanceOhms 1000 .
37
+ :case :beta 100 .
38
+
39
+ :low :inputLabel "low input" ; :inputMv 0 .
40
+ :high :inputLabel "high input" ; :inputMv 5000 .
41
+
42
+ :mvfmt-0 :mvValue 0 ; :text "0.00 V" .
43
+ :mvfmt-200 :mvValue 200 ; :text "0.20 V" .
44
+ :mvfmt-4800 :mvValue 4800 ; :text "4.80 V" .
45
+ :mvfmt-5000 :mvValue 5000 ; :text "5.00 V" .
46
+
47
+ :uafmt-0 :uaValue 0 ; :text "0.00 mA" .
48
+ :uafmt-430 :uaValue 430 ; :text "0.43 mA" .
49
+ :uafmt-4800 :uaValue 4800 ; :text "4.80 mA" .
50
+ :uafmt-43000 :uaValue 43000 ; :text "43.00 mA" .
51
+
52
+ # -----
53
+ # Logic
54
+ # -----
55
+
56
+ # Low-input branch: below turn-on, the transistor stays in cutoff.
57
+ { ?state list:in (:low :high) .
58
+ ?state :inputMv ?vin .
59
+ :case :vbeOnMv ?vbe .
60
+ ?vin math:notGreaterThan ?vbe .
61
+ :case :supplyMv ?vcc .
62
+ }
63
+ => { ?state :baseCurrentUa 0 .
64
+ ?state :collectorGainLimitUa 0 .
65
+ ?state :collectorLoadLimitUa 0 .
66
+ ?state :collectorCurrentUa 0 .
67
+ ?state :loadVoltageMv 0 .
68
+ ?state :collectorEmitterVoltageMv ?vcc .
69
+ ?state :cutoff true .
70
+ ?state :saturation false .
71
+ ?state :stateLabel "cutoff / OFF" . } .
72
+
73
+ # Forward-biased branch: derive base current and the two collector limits.
74
+ { ?state list:in (:low :high) .
75
+ ?state :inputMv ?vin .
76
+ :case :vbeOnMv ?vbe .
77
+ ?vin math:greaterThan ?vbe .
78
+ :case :baseResistanceOhms ?rb .
79
+ ( ?vin ?vbe ) math:difference ?overdriveMv .
80
+ ( ?overdriveMv 1000 ) math:product ?baseNumerator .
81
+ ( ?baseNumerator ?rb ) math:integerQuotient ?baseCurrentUa .
82
+ :case :beta ?beta .
83
+ ( ?baseCurrentUa ?beta ) math:product ?collectorGainLimitUa .
84
+ :case :supplyMv ?vcc .
85
+ :case :vceSatMv ?vceSat .
86
+ :case :loadResistanceOhms ?rl .
87
+ ( ?vcc ?vceSat ) math:difference ?loadSpanMv .
88
+ ( ?loadSpanMv 1000 ) math:product ?loadNumerator .
89
+ ( ?loadNumerator ?rl ) math:integerQuotient ?collectorLoadLimitUa .
90
+ }
91
+ => { ?state :baseCurrentUa ?baseCurrentUa .
92
+ ?state :collectorGainLimitUa ?collectorGainLimitUa .
93
+ ?state :collectorLoadLimitUa ?collectorLoadLimitUa .
94
+ ?state :cutoff false . } .
95
+
96
+ # Saturated ON branch: gain limit is large enough to reach the load limit.
97
+ # Only applies after the forward-biased branch has established cutoff=false.
98
+ { ?state :cutoff false .
99
+ ?state :collectorGainLimitUa ?gainLimit .
100
+ ?state :collectorLoadLimitUa ?loadLimit .
101
+ ?gainLimit math:notLessThan ?loadLimit .
102
+ :case :loadResistanceOhms ?rl .
103
+ ( ?loadLimit ?rl ) math:product ?loadVoltageNumerator .
104
+ ( ?loadVoltageNumerator 1000 ) math:integerQuotient ?loadVoltageMv .
105
+ :case :vceSatMv ?vceSat .
106
+ }
107
+ => { ?state :collectorCurrentUa ?loadLimit .
108
+ ?state :loadVoltageMv ?loadVoltageMv .
109
+ ?state :collectorEmitterVoltageMv ?vceSat .
110
+ ?state :saturation true .
111
+ ?state :stateLabel "saturation / ON" . } .
112
+
113
+ # Active branch: included for completeness, although the sampled high input
114
+ # reaches saturation in this toy case. It also only applies after cutoff=false.
115
+ { ?state :cutoff false .
116
+ ?state :collectorGainLimitUa ?gainLimit .
117
+ ?state :collectorLoadLimitUa ?loadLimit .
118
+ ?gainLimit math:lessThan ?loadLimit .
119
+ :case :loadResistanceOhms ?rl .
120
+ ( ?gainLimit ?rl ) math:product ?loadVoltageNumerator .
121
+ ( ?loadVoltageNumerator 1000 ) math:integerQuotient ?loadVoltageMv .
122
+ :case :supplyMv ?vcc .
123
+ ( ?vcc ?loadVoltageMv ) math:difference ?vceMv .
124
+ }
125
+ => { ?state :collectorCurrentUa ?gainLimit .
126
+ ?state :loadVoltageMv ?loadVoltageMv .
127
+ ?state :collectorEmitterVoltageMv ?vceMv .
128
+ ?state :saturation false .
129
+ ?state :stateLabel "active / linear" . } .
130
+
131
+ # Derived report-level checks.
132
+ { :low :cutoff true .
133
+ :low :baseCurrentUa 0 .
134
+ :low :collectorCurrentUa 0 .
135
+ :low :loadVoltageMv 0 .
136
+ :case :supplyMv ?vcc .
137
+ :low :collectorEmitterVoltageMv ?vcc .
138
+ }
139
+ => { :case :lowInputStaysInCutoff true . } .
140
+
141
+ { :high :saturation true .
142
+ :case :vceSatMv ?vceSat .
143
+ :high :collectorEmitterVoltageMv ?vceSat .
144
+ }
145
+ => { :case :highInputReachesSaturation true . } .
146
+
147
+ { :low :cutoff true .
148
+ :high :saturation true .
149
+ }
150
+ => { :case :switchesCleanly true . } .
151
+
152
+ { :high :collectorCurrentUa ?ic .
153
+ :high :collectorLoadLimitUa ?ic .
154
+ :high :collectorGainLimitUa ?gain .
155
+ ?gain math:greaterThan ?ic .
156
+ }
157
+ => { :case :onStateIsLoadLimited true . } .
158
+
159
+ { :high :collectorCurrentUa ?ic .
160
+ :case :loadResistanceOhms ?rl .
161
+ ( ?ic ?rl ) math:product ?loadVoltageNumerator .
162
+ ( ?loadVoltageNumerator 1000 ) math:integerQuotient ?expectedLoadVoltageMv .
163
+ :high :loadVoltageMv ?expectedLoadVoltageMv .
164
+ }
165
+ => { :case :loadVoltageMatchesResistorDrop true . } .
166
+
167
+ # State summary rendering.
168
+ { ?state list:in (:low :high) .
169
+ ?state :inputMv ?vin .
170
+ ?mvVin :mvValue ?vin ; :text ?vinText .
171
+ ?state :baseCurrentUa ?ib .
172
+ ?uaIb :uaValue ?ib ; :text ?ibText .
173
+ ?state :collectorCurrentUa ?ic .
174
+ ?uaIc :uaValue ?ic ; :text ?icText .
175
+ ?state :collectorEmitterVoltageMv ?vce .
176
+ ?mvVce :mvValue ?vce ; :text ?vceText .
177
+ ?state :stateLabel ?label .
178
+ ( "Vin=%s -> Ib=%s, Ic=%s, Vce=%s, state=%s\n" ?vinText ?ibText ?icText ?vceText ?label ) string:format ?summary .
179
+ }
180
+ => { ?state :summaryLine ?summary . } .
181
+
182
+ # -----------------------------------------------
183
+ # Presentation (ARC: Answer • Reason Why • Check)
184
+ # -----------------------------------------------
185
+
186
+ # Answer
187
+ { :low :stateLabel ?lowLabel .
188
+ :high :stateLabel ?highLabel .
189
+ :high :collectorCurrentUa ?onCurrentUa .
190
+ ?uaOn :uaValue ?onCurrentUa ; :text ?onCurrentText .
191
+ ( "low input state : %s\n" ?lowLabel ) string:format ?lowStateLine .
192
+ ( "high input state : %s\n" ?highLabel ) string:format ?highStateLine .
193
+ ( "on-state load current : %s\n\n" ?onCurrentText ) string:format ?onCurrentLine .
194
+ }
195
+ => {
196
+ :01-answer-1 log:outputString "=== Answer ===\n" .
197
+ :01-answer-2 log:outputString "In this toy transistor-switch model, a low input leaves the transistor in cutoff (OFF) and a high input drives it into saturation (ON), so the load behaves like an on/off branch rather than a linear amplifier.\n" .
198
+ :01-answer-3 log:outputString "case : transistor-switch\n" .
199
+ :01-answer-4 log:outputString ?lowStateLine .
200
+ :01-answer-5 log:outputString ?highStateLine .
201
+ :01-answer-6 log:outputString ?onCurrentLine .
202
+ } .
203
+
204
+ # Reason Why
205
+ { :case :supplyMv ?supplyMv .
206
+ ?mvSupply :mvValue ?supplyMv ; :text ?supplyText .
207
+ :case :baseResistanceOhms ?rb .
208
+ :case :loadResistanceOhms ?rl .
209
+ :case :beta ?beta .
210
+ :low :summaryLine ?lowSummary .
211
+ :high :summaryLine ?highSummary .
212
+ :high :collectorGainLimitUa ?gainLimitUa .
213
+ ?uaGain :uaValue ?gainLimitUa ; :text ?gainLimitText .
214
+ :high :collectorLoadLimitUa ?loadLimitUa .
215
+ ?uaLoad :uaValue ?loadLimitUa ; :text ?loadLimitText .
216
+ ( "supply voltage : %s\n" ?supplyText ) string:format ?supplyLine .
217
+ ( "base resistor : %s ohms\n" ?rb ) string:format ?rbLine .
218
+ ( "load resistor : %s ohms\n" ?rl ) string:format ?rlLine .
219
+ ( "transistor beta proxy : %s\n" ?beta ) string:format ?betaLine .
220
+ ( "low input : %s" ?lowSummary ) string:format ?lowLine .
221
+ ( "high input : %s" ?highSummary ) string:format ?highLine .
222
+ ( "high-input gain limit : %s\n" ?gainLimitText ) string:format ?gainLine .
223
+ ( "high-input load limit : %s\n" ?loadLimitText ) string:format ?loadLine .
224
+ }
225
+ => {
226
+ :02-why-01 log:outputString "=== Reason Why ===\n" .
227
+ :02-why-02 log:outputString "We model an NPN low-side switch with exact millivolt and microamp arithmetic. The base current comes from (Vin - Vbe,on)/Rb when the base-emitter junction is forward biased, and the collector current is the smaller of beta * Ib and the load-limited current (Vcc - Vce,sat)/Rl.\n" .
228
+ :02-why-03 log:outputString ?supplyLine .
229
+ :02-why-04 log:outputString ?rbLine .
230
+ :02-why-05 log:outputString ?rlLine .
231
+ :02-why-06 log:outputString ?betaLine .
232
+ :02-why-07 log:outputString ?lowLine .
233
+ :02-why-08 log:outputString ?highLine .
234
+ :02-why-09 log:outputString ?gainLine .
235
+ :02-why-10 log:outputString ?loadLine .
236
+ :02-why-99 log:outputString "\n" .
237
+ } .
238
+
239
+ # Check
240
+ { :case :lowInputStaysInCutoff true .
241
+ :case :highInputReachesSaturation true .
242
+ :case :switchesCleanly true .
243
+ :case :onStateIsLoadLimited true .
244
+ :case :loadVoltageMatchesResistorDrop true .
245
+ }
246
+ => {
247
+ :03-check-1 log:outputString "=== Check ===\n" .
248
+ :03-check-2 log:outputString "low input stays in cutoff : yes\n" .
249
+ :03-check-3 log:outputString "high input reaches saturation : yes\n" .
250
+ :03-check-4 log:outputString "switching states differ : yes\n" .
251
+ :03-check-5 log:outputString "on-state current is load-limited : yes\n" .
252
+ :03-check-6 log:outputString "load voltage matches resistor drop : yes\n" .
253
+ } .
254
+
255
+ # ------------------
256
+ # Verification fuses
257
+ # ------------------
258
+
259
+ # If any of the key invariants fail, stop the run loudly.
260
+
261
+ { :low :baseCurrentUa ?ib .
262
+ ?ib math:notEqualTo 0 .
263
+ } => false .
264
+
265
+ { :low :collectorCurrentUa ?ic .
266
+ ?ic math:notEqualTo 0 .
267
+ } => false .
268
+
269
+ { :low :loadVoltageMv ?loadVoltageMv .
270
+ ?loadVoltageMv math:notEqualTo 0 .
271
+ } => false .
272
+
273
+ { :low :collectorEmitterVoltageMv ?lowVce .
274
+ :case :supplyMv ?vcc .
275
+ ?lowVce math:notEqualTo ?vcc .
276
+ } => false .
277
+
278
+ { :high :collectorEmitterVoltageMv ?highVce .
279
+ :case :vceSatMv ?vceSat .
280
+ ?highVce math:notEqualTo ?vceSat .
281
+ } => false .
282
+
283
+ { :high :collectorCurrentUa ?ic .
284
+ :high :collectorLoadLimitUa ?loadLimit .
285
+ ?ic math:notEqualTo ?loadLimit .
286
+ } => false .
287
+
288
+ { :high :collectorGainLimitUa ?gain .
289
+ :high :collectorLoadLimitUa ?loadLimit .
290
+ ?gain math:notGreaterThan ?loadLimit .
291
+ } => false .
292
+
293
+ { :high :collectorCurrentUa ?ic .
294
+ :case :loadResistanceOhms ?rl .
295
+ ( ?ic ?rl ) math:product ?loadVoltageNumerator .
296
+ ( ?loadVoltageNumerator 1000 ) math:integerQuotient ?expectedLoadVoltageMv .
297
+ :high :loadVoltageMv ?actualLoadVoltageMv .
298
+ ?expectedLoadVoltageMv math:notEqualTo ?actualLoadVoltageMv .
299
+ } => false .