@winkjs/composer 0.0.2 → 0.4.0
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/LICENSE +1 -1
- package/README.md +42 -103
- package/package.json +53 -29
- package/src/composer.js +75 -0
- package/src/core/batch/aoo-encoder.js +148 -0
- package/src/core/batch/index.js +12 -0
- package/src/core/batch/sliding-window.js +91 -0
- package/src/core/codec/index.js +137 -0
- package/src/core/emitter-manager/mqtt/constants.js +102 -0
- package/src/core/emitter-manager/mqtt/emitter.js +780 -0
- package/src/core/emitter-manager/mqtt/index.js +190 -0
- package/src/core/emitter-manager/mqtt/mqtt-store.js +538 -0
- package/src/core/emitter-manager/terminal/assert-columns.js +98 -0
- package/src/core/emitter-manager/terminal/formatters.js +160 -0
- package/src/core/emitter-manager/terminal/index.js +341 -0
- package/src/core/env-vars.js +245 -0
- package/src/core/mqtt-protocol.js +47 -0
- package/src/core/partition-manager/index.js +7 -0
- package/src/core/partition-manager/init.js +44 -0
- package/src/core/partition-manager/introspect.js +15 -0
- package/src/core/partition-manager/publish-to.js +6 -0
- package/src/core/partition-manager/recompute.js +5 -0
- package/src/core/partition-manager/reset.js +5 -0
- package/src/core/partition-manager/update.js +264 -0
- package/src/core/partition-manager/validate-flow.js +157 -0
- package/src/core/semantics/digest.js +128 -0
- package/src/core/semantics/index.js +83 -0
- package/src/core/semantics/loader.js +571 -0
- package/src/core/semantics/schemas/asset-class-schema.js +218 -0
- package/src/core/semantics/schemas/column-schema.js +651 -0
- package/src/core/semantics/schemas/enum-schema.js +153 -0
- package/src/core/semantics/schemas/index.js +51 -0
- package/src/core/semantics/warnings.js +141 -0
- package/src/core/shutdown-manager/index.js +289 -0
- package/src/core/source-manager/csv/index.js +171 -0
- package/src/core/source-manager/csv/start.js +456 -0
- package/src/core/source-manager/mqtt/client.js +406 -0
- package/src/core/source-manager/mqtt/constants.js +147 -0
- package/src/core/source-manager/mqtt/dedup.js +244 -0
- package/src/core/source-manager/mqtt/index.js +260 -0
- package/src/core/source-manager/mqtt/status.js +546 -0
- package/src/core/source-manager/test-harness/comparator.js +226 -0
- package/src/core/source-manager/test-harness/field-generator.js +85 -0
- package/src/core/source-manager/test-harness/fuzz.js +87 -0
- package/src/core/source-manager/test-harness/index.js +115 -0
- package/src/core/source-manager/test-harness/prng.js +57 -0
- package/src/core/source-manager/test-harness/start.js +232 -0
- package/src/core/source-manager/test-harness/validate.js +213 -0
- package/src/core/storage-manager/questdb/assert-columns.js +120 -0
- package/src/core/storage-manager/questdb/ensure-tables.js +187 -0
- package/src/core/storage-manager/questdb/index.js +1251 -0
- package/src/core/storage-manager/questdb/persist-plan.js +488 -0
- package/src/core/storage-manager/questdb/writers.js +142 -0
- package/src/core/test-utils/sealed-record.js +80 -0
- package/src/core/test-utils/soak-signature.js +149 -0
- package/src/core/test-utils/tcp-proxy.js +106 -0
- package/src/core/tunable/helpers.js +254 -0
- package/src/core/tunable/index.js +87 -0
- package/src/core/utils/console-formatter/index.js +207 -0
- package/src/core/utils/create-smart-store-as/index.js +98 -0
- package/src/core/utils/create-smart-store-as/sanitize-field.js +46 -0
- package/src/core/utils/create-smart-store-as/smart-truncate.js +93 -0
- package/src/core/utils/flow/index.js +5 -0
- package/src/core/utils/flow/make-collision-checker.js +23 -0
- package/src/core/utils/flow/to-kebab.js +20 -0
- package/src/core/utils/half-life/index.js +111 -0
- package/src/core/utils/node/disable.js +11 -0
- package/src/core/utils/node/enable.js +11 -0
- package/src/core/utils/node/execute-triggers.js +40 -0
- package/src/core/utils/node/index.js +10 -0
- package/src/core/utils/node/pause.js +11 -0
- package/src/core/utils/node/populate-predicate-inputs.js +14 -0
- package/src/core/utils/node/publish-nan.js +19 -0
- package/src/core/utils/node/reset-predicate-inputs.js +11 -0
- package/src/core/utils/node/unpause.js +11 -0
- package/src/core/utils/node/validate-spec.js +22 -0
- package/src/core/utils/options/resolve-field-keyed.js +115 -0
- package/src/core/utils/quantize/index.js +75 -0
- package/src/core/utils/stats/formulas.js +67 -0
- package/src/core/utils/stats/index.js +3 -0
- package/src/core/utils/validate/composer-validators.js +141 -0
- package/src/core/utils/validate/helpers.js +206 -0
- package/src/core/utils/validate/index.js +12 -0
- package/src/core/utils/validate/type-validators.js +538 -0
- package/src/core/utils/validate/validate.js +107 -0
- package/src/core/utils/validate/validators.js +177 -0
- package/src/core/wiring/assert-handle.js +57 -0
- package/src/core/wiring/assert-module.js +63 -0
- package/src/core/wiring/index.js +5 -0
- package/src/core/wiring/node-type-to-module.js +17 -0
- package/src/core/wiring/wire-emitters.js +256 -0
- package/src/core/wiring/wire-linear-graph.js +25 -0
- package/src/core/wiring/wire-node.js +157 -0
- package/src/core/wiring/wire-semantics.js +173 -0
- package/src/core/wiring/wire-storages.js +344 -0
- package/src/flow/check-output-collisions.js +93 -0
- package/src/flow/consts.js +9 -0
- package/src/flow/deep-clone-spec.js +64 -0
- package/src/flow/driver.js +171 -0
- package/src/flow/expand-single-input-array.js +76 -0
- package/src/flow/expand-template-spec.js +93 -0
- package/src/flow/flow-api-schemas.js +300 -0
- package/src/flow/flow.js +658 -0
- package/src/flow/for-each.js +225 -0
- package/src/flow/get-signature-pattern.js +31 -0
- package/src/flow/get-target-array.js +46 -0
- package/src/flow/inspect.js +67 -0
- package/src/flow/load-node-module.js +81 -0
- package/src/flow/make-node-method.js +112 -0
- package/src/flow/resolve-pick-by-field.js +77 -0
- package/src/flow/resolve-tunables.js +76 -0
- package/src/flow/run.js +392 -0
- package/src/flow/serialize.js +161 -0
- package/src/flow/signatures-schema.js +222 -0
- package/src/flow/spec-builder.js +65 -0
- package/src/flow/validate-triggers.js +250 -0
- package/src/flow/validate.js +261 -0
- package/src/nodes/accumulate/index.js +21 -0
- package/src/nodes/accumulate/init.js +48 -0
- package/src/nodes/accumulate/introspect.js +123 -0
- package/src/nodes/accumulate/publish-to.js +34 -0
- package/src/nodes/accumulate/recompute.js +27 -0
- package/src/nodes/accumulate/reset.js +25 -0
- package/src/nodes/accumulate/update.js +42 -0
- package/src/nodes/appraise/calibrate.js +64 -0
- package/src/nodes/appraise/decision.js +66 -0
- package/src/nodes/appraise/deviation.js +140 -0
- package/src/nodes/appraise/index.js +23 -0
- package/src/nodes/appraise/init.js +165 -0
- package/src/nodes/appraise/integrate.js +45 -0
- package/src/nodes/appraise/introspect.js +352 -0
- package/src/nodes/appraise/publish-to.js +93 -0
- package/src/nodes/appraise/receptor.js +69 -0
- package/src/nodes/appraise/recompute.js +79 -0
- package/src/nodes/appraise/reset.js +53 -0
- package/src/nodes/appraise/update.js +130 -0
- package/src/nodes/archive/csv/index.js +6 -0
- package/src/nodes/archive/csv/init.js +36 -0
- package/src/nodes/archive/csv/load.js +70 -0
- package/src/nodes/archive/csv/reset.js +14 -0
- package/src/nodes/archive/csv/simple-parser.js +216 -0
- package/src/nodes/archive/csv/test.js +173 -0
- package/src/nodes/archive/csv/update.js +63 -0
- package/src/nodes/butterworth-filter/index.js +18 -0
- package/src/nodes/butterworth-filter/init.js +184 -0
- package/src/nodes/butterworth-filter/introspect.js +228 -0
- package/src/nodes/butterworth-filter/publish-to.js +21 -0
- package/src/nodes/butterworth-filter/recompute.js +13 -0
- package/src/nodes/butterworth-filter/reset.js +30 -0
- package/src/nodes/butterworth-filter/update.js +38 -0
- package/src/nodes/categorize/index.js +19 -0
- package/src/nodes/categorize/init.js +58 -0
- package/src/nodes/categorize/introspect.js +177 -0
- package/src/nodes/categorize/publish-to.js +29 -0
- package/src/nodes/categorize/recompute.js +14 -0
- package/src/nodes/categorize/reset.js +16 -0
- package/src/nodes/categorize/update.js +53 -0
- package/src/nodes/controller/index.js +9 -0
- package/src/nodes/controller/init.js +55 -0
- package/src/nodes/controller/introspect.js +91 -0
- package/src/nodes/controller/publish-to.js +8 -0
- package/src/nodes/controller/recompute.js +9 -0
- package/src/nodes/controller/reset.js +17 -0
- package/src/nodes/controller/update.js +63 -0
- package/src/nodes/diff/index.js +19 -0
- package/src/nodes/diff/init.js +37 -0
- package/src/nodes/diff/introspect.js +132 -0
- package/src/nodes/diff/publish-to.js +21 -0
- package/src/nodes/diff/recompute.js +12 -0
- package/src/nodes/diff/reset.js +12 -0
- package/src/nodes/diff/update.js +37 -0
- package/src/nodes/digest-moments/formulas.js +12 -0
- package/src/nodes/digest-moments/index.js +14 -0
- package/src/nodes/digest-moments/init.js +67 -0
- package/src/nodes/digest-moments/introspect.js +154 -0
- package/src/nodes/digest-moments/publish-to.js +34 -0
- package/src/nodes/digest-moments/recompute.js +14 -0
- package/src/nodes/digest-moments/reset.js +14 -0
- package/src/nodes/digest-moments/update.js +120 -0
- package/src/nodes/dwell-time-tracker/index.js +14 -0
- package/src/nodes/dwell-time-tracker/init.js +59 -0
- package/src/nodes/dwell-time-tracker/introspect.js +137 -0
- package/src/nodes/dwell-time-tracker/publish-to.js +22 -0
- package/src/nodes/dwell-time-tracker/recompute.js +8 -0
- package/src/nodes/dwell-time-tracker/reset.js +23 -0
- package/src/nodes/dwell-time-tracker/update.js +103 -0
- package/src/nodes/emit-if/helpers.js +94 -0
- package/src/nodes/emit-if/index.js +16 -0
- package/src/nodes/emit-if/init.js +67 -0
- package/src/nodes/emit-if/introspect.js +102 -0
- package/src/nodes/emit-if/publish-to.js +13 -0
- package/src/nodes/emit-if/recompute.js +12 -0
- package/src/nodes/emit-if/reset.js +27 -0
- package/src/nodes/emit-if/update.js +93 -0
- package/src/nodes/es-correlation/index.js +19 -0
- package/src/nodes/es-correlation/init.js +73 -0
- package/src/nodes/es-correlation/introspect.js +176 -0
- package/src/nodes/es-correlation/publish-to.js +32 -0
- package/src/nodes/es-correlation/recompute.js +56 -0
- package/src/nodes/es-correlation/reset.js +27 -0
- package/src/nodes/es-correlation/update.js +110 -0
- package/src/nodes/es-mean/index.js +20 -0
- package/src/nodes/es-mean/init.js +58 -0
- package/src/nodes/es-mean/introspect.js +129 -0
- package/src/nodes/es-mean/publish-to.js +19 -0
- package/src/nodes/es-mean/recompute.js +16 -0
- package/src/nodes/es-mean/reset.js +24 -0
- package/src/nodes/es-mean/update.js +69 -0
- package/src/nodes/es-pairwise-correlation/index.js +19 -0
- package/src/nodes/es-pairwise-correlation/init.js +98 -0
- package/src/nodes/es-pairwise-correlation/introspect.js +187 -0
- package/src/nodes/es-pairwise-correlation/publish-to.js +31 -0
- package/src/nodes/es-pairwise-correlation/recompute.js +69 -0
- package/src/nodes/es-pairwise-correlation/reset.js +29 -0
- package/src/nodes/es-pairwise-correlation/update.js +89 -0
- package/src/nodes/es-stats/index.js +21 -0
- package/src/nodes/es-stats/init.js +105 -0
- package/src/nodes/es-stats/introspect.js +157 -0
- package/src/nodes/es-stats/publish-to.js +42 -0
- package/src/nodes/es-stats/recompute.js +74 -0
- package/src/nodes/es-stats/reset.js +49 -0
- package/src/nodes/es-stats/update-envelope.js +44 -0
- package/src/nodes/es-stats/update-welford.js +64 -0
- package/src/nodes/es-stats/update.js +59 -0
- package/src/nodes/index.js +83 -0
- package/src/nodes/invert-flag/index.js +20 -0
- package/src/nodes/invert-flag/init.js +36 -0
- package/src/nodes/invert-flag/introspect.js +115 -0
- package/src/nodes/invert-flag/publish-to.js +23 -0
- package/src/nodes/invert-flag/recompute.js +12 -0
- package/src/nodes/invert-flag/reset.js +12 -0
- package/src/nodes/invert-flag/update.js +32 -0
- package/src/nodes/kalman1d/index.js +20 -0
- package/src/nodes/kalman1d/init.js +103 -0
- package/src/nodes/kalman1d/introspect.js +250 -0
- package/src/nodes/kalman1d/publish-to.js +34 -0
- package/src/nodes/kalman1d/recompute.js +23 -0
- package/src/nodes/kalman1d/reset.js +29 -0
- package/src/nodes/kalman1d/update.js +144 -0
- package/src/nodes/kernel/index.js +18 -0
- package/src/nodes/kernel/init.js +66 -0
- package/src/nodes/kernel/introspect.js +163 -0
- package/src/nodes/kernel/presets.js +74 -0
- package/src/nodes/kernel/publish-to.js +25 -0
- package/src/nodes/kernel/recompute.js +14 -0
- package/src/nodes/kernel/reset.js +20 -0
- package/src/nodes/kernel/update.js +69 -0
- package/src/nodes/lag/index.js +21 -0
- package/src/nodes/lag/init.js +87 -0
- package/src/nodes/lag/introspect.js +202 -0
- package/src/nodes/lag/publish-to.js +64 -0
- package/src/nodes/lag/recompute.js +33 -0
- package/src/nodes/lag/reset.js +49 -0
- package/src/nodes/lag/update.js +133 -0
- package/src/nodes/median3/index.js +19 -0
- package/src/nodes/median3/init.js +40 -0
- package/src/nodes/median3/introspect.js +116 -0
- package/src/nodes/median3/publish-to.js +21 -0
- package/src/nodes/median3/recompute.js +12 -0
- package/src/nodes/median3/reset.js +15 -0
- package/src/nodes/median3/update.js +58 -0
- package/src/nodes/moments-digest/copy-from.js +12 -0
- package/src/nodes/moments-digest/flush.js +17 -0
- package/src/nodes/moments-digest/index.js +132 -0
- package/src/nodes/moments-digest/init.js +84 -0
- package/src/nodes/moments-digest/introspect.js +122 -0
- package/src/nodes/moments-digest/prelude.js +28 -0
- package/src/nodes/moments-digest/publish-to.js +31 -0
- package/src/nodes/moments-digest/recompute.js +9 -0
- package/src/nodes/moments-digest/reset.js +20 -0
- package/src/nodes/moments-digest/update.js +182 -0
- package/src/nodes/node-names.js +67 -0
- package/src/nodes/page-hinkley/index.js +20 -0
- package/src/nodes/page-hinkley/init.js +66 -0
- package/src/nodes/page-hinkley/introspect.js +193 -0
- package/src/nodes/page-hinkley/publish-to.js +28 -0
- package/src/nodes/page-hinkley/recompute.js +14 -0
- package/src/nodes/page-hinkley/reset.js +23 -0
- package/src/nodes/page-hinkley/update.js +105 -0
- package/src/nodes/pass-if/index.js +21 -0
- package/src/nodes/pass-if/init.js +37 -0
- package/src/nodes/pass-if/introspect.js +93 -0
- package/src/nodes/pass-if/publish-to.js +15 -0
- package/src/nodes/pass-if/recompute.js +15 -0
- package/src/nodes/pass-if/reset.js +18 -0
- package/src/nodes/pass-if/update.js +37 -0
- package/src/nodes/persist-if/helpers.js +111 -0
- package/src/nodes/persist-if/index.js +9 -0
- package/src/nodes/persist-if/init.js +98 -0
- package/src/nodes/persist-if/introspect.js +106 -0
- package/src/nodes/persist-if/publish-to.js +21 -0
- package/src/nodes/persist-if/recompute.js +20 -0
- package/src/nodes/persist-if/reset.js +35 -0
- package/src/nodes/persist-if/update.js +112 -0
- package/src/nodes/persistence-check/index.js +14 -0
- package/src/nodes/persistence-check/init.js +38 -0
- package/src/nodes/persistence-check/introspect.js +144 -0
- package/src/nodes/persistence-check/publish-to.js +11 -0
- package/src/nodes/persistence-check/recompute.js +7 -0
- package/src/nodes/persistence-check/reset.js +12 -0
- package/src/nodes/persistence-check/update.js +57 -0
- package/src/nodes/process-index/index.js +15 -0
- package/src/nodes/process-index/init.js +48 -0
- package/src/nodes/process-index/introspect.js +182 -0
- package/src/nodes/process-index/publish-to.js +29 -0
- package/src/nodes/process-index/recompute.js +8 -0
- package/src/nodes/process-index/reset.js +8 -0
- package/src/nodes/process-index/update.js +75 -0
- package/src/nodes/ratio/index.js +14 -0
- package/src/nodes/ratio/init.js +37 -0
- package/src/nodes/ratio/introspect.js +155 -0
- package/src/nodes/ratio/publish-to.js +15 -0
- package/src/nodes/ratio/recompute.js +7 -0
- package/src/nodes/ratio/reset.js +7 -0
- package/src/nodes/ratio/update.js +47 -0
- package/src/nodes/sanitize/index.js +82 -0
- package/src/nodes/sanitize/init.js +82 -0
- package/src/nodes/sanitize/introspect.js +194 -0
- package/src/nodes/sanitize/publish-to.js +15 -0
- package/src/nodes/sanitize/recompute.js +8 -0
- package/src/nodes/sanitize/reset.js +14 -0
- package/src/nodes/sanitize/update.js +102 -0
- package/src/nodes/spike-guard/index.js +14 -0
- package/src/nodes/spike-guard/init.js +47 -0
- package/src/nodes/spike-guard/introspect.js +130 -0
- package/src/nodes/spike-guard/publish-to.js +35 -0
- package/src/nodes/spike-guard/recompute.js +14 -0
- package/src/nodes/spike-guard/reset.js +19 -0
- package/src/nodes/spike-guard/update.js +89 -0
- package/src/nodes/sse-emitter/index.js +7 -0
- package/src/nodes/sse-emitter/init.js +105 -0
- package/src/nodes/sse-emitter/introspect.js +134 -0
- package/src/nodes/sse-emitter/publish-to.js +6 -0
- package/src/nodes/sse-emitter/recompute.js +16 -0
- package/src/nodes/sse-emitter/reset.js +19 -0
- package/src/nodes/sse-emitter/update.js +69 -0
- package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +36 -0
- package/src/nodes/sse-emitter/utils/build-message.js +31 -0
- package/src/nodes/sse-emitter/utils/connection-manager.js +45 -0
- package/src/nodes/sse-emitter/utils/endpoint-initializer.js +20 -0
- package/src/nodes/sse-emitter/utils/health-monitor.js +38 -0
- package/src/nodes/sse-emitter/utils/qos-handler.js +41 -0
- package/src/nodes/state-change-detector/helpers.js +35 -0
- package/src/nodes/state-change-detector/index.js +12 -0
- package/src/nodes/state-change-detector/init.js +56 -0
- package/src/nodes/state-change-detector/introspect.js +146 -0
- package/src/nodes/state-change-detector/publish-to.js +20 -0
- package/src/nodes/state-change-detector/recompute.js +6 -0
- package/src/nodes/state-change-detector/reset.js +20 -0
- package/src/nodes/state-change-detector/update.js +97 -0
- package/src/nodes/sw-stats/index.js +14 -0
- package/src/nodes/sw-stats/init.js +51 -0
- package/src/nodes/sw-stats/introspect.js +131 -0
- package/src/nodes/sw-stats/publish-to.js +45 -0
- package/src/nodes/sw-stats/recompute.js +33 -0
- package/src/nodes/sw-stats/reset.js +15 -0
- package/src/nodes/sw-stats/update.js +39 -0
- package/src/nodes/swing-watch/index.js +17 -0
- package/src/nodes/swing-watch/init.js +144 -0
- package/src/nodes/swing-watch/introspect.js +188 -0
- package/src/nodes/swing-watch/publish-to.js +53 -0
- package/src/nodes/swing-watch/recompute.js +22 -0
- package/src/nodes/swing-watch/reset.js +55 -0
- package/src/nodes/swing-watch/update.js +767 -0
- package/src/nodes/tally/index.js +15 -0
- package/src/nodes/tally/init.js +32 -0
- package/src/nodes/tally/introspect.js +116 -0
- package/src/nodes/tally/publish-to.js +28 -0
- package/src/nodes/tally/recompute.js +10 -0
- package/src/nodes/tally/reset.js +10 -0
- package/src/nodes/tally/update.js +44 -0
- package/src/nodes/threshold/index.js +14 -0
- package/src/nodes/threshold/init.js +121 -0
- package/src/nodes/threshold/introspect.js +192 -0
- package/src/nodes/threshold/publish-to.js +11 -0
- package/src/nodes/threshold/recompute.js +8 -0
- package/src/nodes/threshold/reset.js +14 -0
- package/src/nodes/threshold/update.js +54 -0
- package/src/nodes/transform/helpers.js +39 -0
- package/src/nodes/transform/index.js +15 -0
- package/src/nodes/transform/init.js +35 -0
- package/src/nodes/transform/introspect.js +118 -0
- package/src/nodes/transform/publish-to.js +23 -0
- package/src/nodes/transform/recompute.js +12 -0
- package/src/nodes/transform/reset.js +11 -0
- package/src/nodes/transform/update.js +32 -0
- package/src/nodes/trend/compute-confidence.js +112 -0
- package/src/nodes/trend/index.js +14 -0
- package/src/nodes/trend/init.js +81 -0
- package/src/nodes/trend/introspect.js +151 -0
- package/src/nodes/trend/publish-to.js +22 -0
- package/src/nodes/trend/recompute.js +15 -0
- package/src/nodes/trend/reset.js +26 -0
- package/src/nodes/trend/update.js +93 -0
- package/src/nodes/tw-stats/copy-from.js +16 -0
- package/src/nodes/tw-stats/flush.js +12 -0
- package/src/nodes/tw-stats/index.js +41 -0
- package/src/nodes/tw-stats/init.js +92 -0
- package/src/nodes/tw-stats/introspect.js +171 -0
- package/src/nodes/tw-stats/publish-to.js +108 -0
- package/src/nodes/tw-stats/recompute.js +9 -0
- package/src/nodes/tw-stats/reset.js +16 -0
- package/src/nodes/tw-stats/update.js +113 -0
- package/src/nodes/unbalance/index.js +15 -0
- package/src/nodes/unbalance/init.js +59 -0
- package/src/nodes/unbalance/introspect.js +167 -0
- package/src/nodes/unbalance/publish-to.js +52 -0
- package/src/nodes/unbalance/recompute.js +11 -0
- package/src/nodes/unbalance/reset.js +10 -0
- package/src/nodes/unbalance/update.js +83 -0
- package/src/nodes/vector-distance/index.js +14 -0
- package/src/nodes/vector-distance/init.js +54 -0
- package/src/nodes/vector-distance/introspect.js +149 -0
- package/src/nodes/vector-distance/publish-to.js +35 -0
- package/src/nodes/vector-distance/recompute.js +11 -0
- package/src/nodes/vector-distance/reset.js +20 -0
- package/src/nodes/vector-distance/update.js +139 -0
- package/src/nodes/winnow/index.js +12 -0
- package/src/nodes/winnow/init.js +82 -0
- package/src/nodes/winnow/introspect.js +253 -0
- package/src/nodes/winnow/publish-to.js +51 -0
- package/src/nodes/winnow/recompute.js +14 -0
- package/src/nodes/winnow/reset.js +34 -0
- package/src/nodes/winnow/update.js +203 -0
- package/src/tools/training/features.js +227 -0
- package/src/tools/training/index.js +21 -0
- package/src/tools/training/logistic.js +238 -0
- package/src/tools/training/metrics.js +126 -0
- package/src/tools/training/scale.js +239 -0
- package/src/tools/training/validate.js +103 -0
- package/src/windowing/count-sliding/create.js +16 -0
- package/src/windowing/count-sliding/get-reverse-chronological-index.js +19 -0
- package/src/windowing/count-sliding/index.js +6 -0
- package/src/windowing/count-sliding/is-not-full.js +5 -0
- package/src/windowing/count-sliding/push.js +15 -0
- package/src/windowing/count-sliding/reset.js +11 -0
- package/src/windowing/count-sliding/size.js +5 -0
- package/src/windowing/time-sliding/create.js +25 -0
- package/src/windowing/time-sliding/push.js +42 -0
- package/CODE_OF_CONDUCT.md +0 -46
- package/CONTRIBUTING.md +0 -132
- package/docs/.nojekyll +0 -0
- package/docs/CONTRIBUTING.md +0 -134
- package/docs/README.md +0 -132
- package/docs/_coverpage.md +0 -26
- package/docs/_media/bg.png +0 -0
- package/docs/_media/wink-logo.png +0 -0
- package/docs/_navbar.md +0 -14
- package/docs/_sidebar.md +0 -2
- package/docs/index.html +0 -44
- package/eslint.config.js +0 -283
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
// @fileoverview
|
|
2
|
+
// update.js — hot path for swingWatch.
|
|
3
|
+
//
|
|
4
|
+
// ## What this file does
|
|
5
|
+
//
|
|
6
|
+
// On each new sample, this function maintains a sliding window (ring buffer)
|
|
7
|
+
// of the last W samples, then ranks every local extremum inside that window
|
|
8
|
+
// by its **topological persistence** — the peak-to-trough amplitude required
|
|
9
|
+
// to "kill" the feature. Only NEWLY-APPEARED persistence pairs (compared to
|
|
10
|
+
// the previous tick's diagram) are emitted as completion events. This design
|
|
11
|
+
// ensures downstream nodes like `appraise` see each significant event exactly
|
|
12
|
+
// once, on the tick it first becomes resolvable.
|
|
13
|
+
//
|
|
14
|
+
// ## Algorithm — Huber/Persistence1D, windowed batch
|
|
15
|
+
//
|
|
16
|
+
// The algorithm is the standard 1D sublevel-set persistence sweep, originally
|
|
17
|
+
// described for peak detection by Stefan Huber and implemented in Persistence1D
|
|
18
|
+
// by Kozlov & Weinkauf. It proceeds in three stages:
|
|
19
|
+
//
|
|
20
|
+
// 1. SORT the window's sample indices by value (ascending for minima,
|
|
21
|
+
// descending for maxima). This defines the order in which a rising
|
|
22
|
+
// "water level" sweeps through the signal.
|
|
23
|
+
//
|
|
24
|
+
// 2. SWEEP through the sorted indices. Each sample either:
|
|
25
|
+
// - Seeds a new connected component (if neither neighbour is processed)
|
|
26
|
+
// - Extends an existing component (if exactly one neighbour is processed)
|
|
27
|
+
// - Merges two components (if both neighbours are processed) — this is
|
|
28
|
+
// the "killing" event. The Elder Rule determines which component dies:
|
|
29
|
+
// the one whose birth value is HIGHER (= younger in the sublevel sweep).
|
|
30
|
+
// Persistence = merge height − dying component's birth value.
|
|
31
|
+
//
|
|
32
|
+
// 3. DIFF the resulting persistence diagram against the previous tick's
|
|
33
|
+
// diagram. Pairs that appear for the first time are emitted as completion
|
|
34
|
+
// events. Pairs that were already present (same physical samples, shifted
|
|
35
|
+
// by one in the window) are suppressed — and so are pairs minted by
|
|
36
|
+
// either window boundary (eviction re-formations at the left edge,
|
|
37
|
+
// provisional in-progress extrema at the right edge). See Section 3
|
|
38
|
+
// for the four suppression rules that guard the exactly-once contract.
|
|
39
|
+
//
|
|
40
|
+
// For maxima detection, the same algorithm runs on the negated signal — but
|
|
41
|
+
// instead of negating and re-sorting, we iterate the SAME sorted indices in
|
|
42
|
+
// reverse order (descending value = ascending in −f) and flip the Elder Rule.
|
|
43
|
+
//
|
|
44
|
+
// ## Performance
|
|
45
|
+
//
|
|
46
|
+
// - Complexity: O(W log W) per tick, dominated by the sort. V8's Timsort on
|
|
47
|
+
// a nearly-sorted array (one element changed per tick) approaches O(W).
|
|
48
|
+
// The union-find sweep is O(W α(W)) ≈ O(W).
|
|
49
|
+
// - At W=100 on M4 Max: ~11 µs/tick. On RPi (est. 6× slower): ~66 µs/tick.
|
|
50
|
+
// Headroom: 15K msg/sec on RPi, vs WiFi's 0.33 msg/sec or SCADA's 50.
|
|
51
|
+
//
|
|
52
|
+
// ## Zero allocation
|
|
53
|
+
//
|
|
54
|
+
// Every typed array is pre-allocated in init.js. This file creates no objects,
|
|
55
|
+
// arrays, or strings in the hot path. The single module-level object `mergeResult` is
|
|
56
|
+
// allocated once at import time and rewritten in-place on each merge — safe
|
|
57
|
+
// because the sweep is single-threaded and synchronous.
|
|
58
|
+
//
|
|
59
|
+
// ## References
|
|
60
|
+
//
|
|
61
|
+
// Huber, S. "Persistent Topology for Peak Detection."
|
|
62
|
+
// https://www.sthu.org/blog/13-perstopology-peakdetection/index.html
|
|
63
|
+
// Edelsbrunner, Letscher & Zomorodian (2002). "Topological Persistence
|
|
64
|
+
// and Simplification." Discrete & Computational Geometry, 28, 511–533.
|
|
65
|
+
// Kozlov & Weinkauf (2014). Persistence1D.
|
|
66
|
+
// https://github.com/weinkauf/Persistence1D
|
|
67
|
+
|
|
68
|
+
import { push, isNotFull } from '../../windowing/count-sliding/index.js';
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// SECTION 1: UNION-FIND
|
|
73
|
+
//
|
|
74
|
+
// The union-find (disjoint-set) data structure tracks connected components
|
|
75
|
+
// during the sweep. Each component is identified by a representative vertex.
|
|
76
|
+
// The representative carries the component's "birth value" — the value of
|
|
77
|
+
// the deepest local extremum that seeded it.
|
|
78
|
+
//
|
|
79
|
+
// Three parallel typed arrays (pre-allocated in init):
|
|
80
|
+
// ufParent[i] — parent pointer (self-referential for roots)
|
|
81
|
+
// ufBirthVal[i] — birth value of the component rooted at i
|
|
82
|
+
// ufBirthIdx[i] — birth index of the component rooted at i
|
|
83
|
+
//
|
|
84
|
+
// Path compression in find() ensures amortized near-O(1) per operation.
|
|
85
|
+
// No allocation: find() and merge write only to the pre-allocated arrays.
|
|
86
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
87
|
+
|
|
88
|
+
// Module-level merge result object. Allocated once at import time, rewritten
|
|
89
|
+
// in-place by mergeSublevel/mergeSuperlevel. The caller reads the fields
|
|
90
|
+
// immediately before any subsequent merge call could overwrite them.
|
|
91
|
+
const mergeResult = { diedBirthVal: 0, diedBirthIdx: 0, persistence: 0, merged: false };
|
|
92
|
+
|
|
93
|
+
// Find the representative of the component containing vertex i.
|
|
94
|
+
// Uses path compression: after find(), every vertex on the path points
|
|
95
|
+
// directly to the root, making subsequent finds O(1).
|
|
96
|
+
const find = function ( parent, i ) {
|
|
97
|
+
let r = i;
|
|
98
|
+
while ( parent[ r ] !== r ) r = parent[ r ];
|
|
99
|
+
// Compress: point every visited node directly to root
|
|
100
|
+
let c = i;
|
|
101
|
+
while ( parent[ c ] !== r ) {
|
|
102
|
+
const next = parent[ c ];
|
|
103
|
+
parent[ c ] = r;
|
|
104
|
+
c = next;
|
|
105
|
+
}
|
|
106
|
+
return r;
|
|
107
|
+
}; // find()
|
|
108
|
+
|
|
109
|
+
// Merge two components WITHOUT emitting a persistence pair.
|
|
110
|
+
// Used for the "extension" case: when the current vertex has exactly one
|
|
111
|
+
// processed neighbour, it simply joins that neighbour's component. No
|
|
112
|
+
// component dies, so no pair is emitted.
|
|
113
|
+
//
|
|
114
|
+
// The Elder Rule still applies to decide which representative survives.
|
|
115
|
+
// For sublevel (ascending): lower birth value = older = survives.
|
|
116
|
+
// For superlevel (descending): higher birth value = older = survives.
|
|
117
|
+
// Ties are broken by birth index: earlier (smaller index) survives.
|
|
118
|
+
|
|
119
|
+
// Sublevel extension: higher birth value dies (younger in ascending sweep).
|
|
120
|
+
// Called as (p, p±1) from sweepMin. Only the `bVal[ra] > bVal[rb]` branch
|
|
121
|
+
// and the plateau tie-break's `bIdx[ra] > bIdx[rb]` branch fire in 1D:
|
|
122
|
+
// - ra = current vertex p (being processed), bVal[ra] = f(p).
|
|
123
|
+
// - rb = already-processed neighbor's root, bVal[rb] ≤ f(p).
|
|
124
|
+
// - Equal values only on plateaus, where extension always goes left-to-right
|
|
125
|
+
// (sublevel sorts ascending by idx within ties), so bIdx[ra]=p > bIdx[rb].
|
|
126
|
+
const mergeNoEmitSub = function ( parent, birthVal, birthIdx, a, b ) {
|
|
127
|
+
const ra = find( parent, a );
|
|
128
|
+
const rb = find( parent, b );
|
|
129
|
+
|
|
130
|
+
/* c8 ignore next -- defensive: left and right components are spatially disjoint in 1D sweep, so ra !== rb */
|
|
131
|
+
if ( ra === rb ) return;
|
|
132
|
+
if ( birthVal[ ra ] > birthVal[ rb ] ) {
|
|
133
|
+
parent[ ra ] = rb;
|
|
134
|
+
|
|
135
|
+
/* c8 ignore next 2 -- defensive: in 1D sublevel sweep, current vertex always has bVal ≥ processed neighbor's bVal */
|
|
136
|
+
} else if ( birthVal[ rb ] > birthVal[ ra ] ) {
|
|
137
|
+
parent[ rb ] = ra;
|
|
138
|
+
} else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
|
|
139
|
+
parent[ ra ] = rb;
|
|
140
|
+
|
|
141
|
+
/* c8 ignore next 3 -- defensive: sublevel plateau extension always has bIdx[ra]=p > bIdx[rb] */
|
|
142
|
+
} else {
|
|
143
|
+
parent[ rb ] = ra;
|
|
144
|
+
}
|
|
145
|
+
}; // mergeNoEmitSub()
|
|
146
|
+
|
|
147
|
+
// Superlevel extension: lower birth value dies (younger in descending sweep).
|
|
148
|
+
// Called as (p, p±1) from sweepMax, which iterates sortedIndices backward.
|
|
149
|
+
// Only the `bVal[ra] < bVal[rb]` branch and the plateau tie-break's ELSE
|
|
150
|
+
// branch fire in 1D:
|
|
151
|
+
// - Current vertex always has bVal ≤ processed neighbor.
|
|
152
|
+
// - Equal values only on plateaus, where backward iteration makes highest
|
|
153
|
+
// idx the seed; subsequent lower-idx extensions go right, giving
|
|
154
|
+
// bIdx[ra]=p < bIdx[rb].
|
|
155
|
+
const mergeNoEmitSup = function ( parent, birthVal, birthIdx, a, b ) {
|
|
156
|
+
const ra = find( parent, a );
|
|
157
|
+
const rb = find( parent, b );
|
|
158
|
+
|
|
159
|
+
/* c8 ignore next -- defensive: left and right components are spatially disjoint in 1D sweep */
|
|
160
|
+
if ( ra === rb ) return;
|
|
161
|
+
if ( birthVal[ ra ] < birthVal[ rb ] ) {
|
|
162
|
+
parent[ ra ] = rb;
|
|
163
|
+
|
|
164
|
+
/* c8 ignore next 3 -- defensive: in 1D superlevel sweep, current vertex always has bVal ≤ processed neighbor's bVal */
|
|
165
|
+
} else if ( birthVal[ rb ] < birthVal[ ra ] ) {
|
|
166
|
+
parent[ rb ] = ra;
|
|
167
|
+
|
|
168
|
+
/* c8 ignore next 3 -- defensive: superlevel plateau extension always has bIdx[ra]=p < bIdx[rb] (backward iteration) */
|
|
169
|
+
} else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
|
|
170
|
+
parent[ ra ] = rb;
|
|
171
|
+
} else {
|
|
172
|
+
parent[ rb ] = ra;
|
|
173
|
+
}
|
|
174
|
+
}; // mergeNoEmitSup()
|
|
175
|
+
|
|
176
|
+
// Merge two components WITH pair emission — SUBLEVEL sweep (ascending).
|
|
177
|
+
//
|
|
178
|
+
// This is called at a "merge point" where both neighbours of the current
|
|
179
|
+
// vertex are already processed. Two distinct components meet for the first
|
|
180
|
+
// time. The Elder Rule kills the younger component (higher birth value).
|
|
181
|
+
// The dying component's birth is paired with the current vertex (the merge
|
|
182
|
+
// height). Persistence = mergeHeight − dyingBirthValue.
|
|
183
|
+
//
|
|
184
|
+
// The result is written to the module-level mergeResult object (no allocation).
|
|
185
|
+
const mergeSublevel = function ( parent, birthVal, birthIdx, a, b, h ) {
|
|
186
|
+
const ra = find( parent, a );
|
|
187
|
+
const rb = find( parent, b );
|
|
188
|
+
|
|
189
|
+
/* c8 ignore next 4 -- defensive: LEFT and RIGHT components are disjoint in 1D before this merge point p, so ra !== rb */
|
|
190
|
+
if ( ra === rb ) {
|
|
191
|
+
mergeResult.merged = false;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
let loser;
|
|
195
|
+
let winner;
|
|
196
|
+
if ( birthVal[ ra ] > birthVal[ rb ] ) {
|
|
197
|
+
loser = ra;
|
|
198
|
+
winner = rb;
|
|
199
|
+
} else if ( birthVal[ rb ] > birthVal[ ra ] ) {
|
|
200
|
+
loser = rb;
|
|
201
|
+
winner = ra;
|
|
202
|
+
|
|
203
|
+
/* c8 ignore next 3 -- defensive: ra roots the LEFT component (bIdx in [0, p-1]), rb roots the RIGHT (bIdx in [p+1, W-1]), so bIdx[ra] < bIdx[rb] always */
|
|
204
|
+
} else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
|
|
205
|
+
loser = ra;
|
|
206
|
+
winner = rb;
|
|
207
|
+
} else {
|
|
208
|
+
loser = rb;
|
|
209
|
+
winner = ra;
|
|
210
|
+
}
|
|
211
|
+
mergeResult.diedBirthVal = birthVal[ loser ];
|
|
212
|
+
mergeResult.diedBirthIdx = birthIdx[ loser ];
|
|
213
|
+
mergeResult.persistence = h - birthVal[ loser ];
|
|
214
|
+
mergeResult.merged = true;
|
|
215
|
+
parent[ loser ] = winner;
|
|
216
|
+
}; // mergeSublevel()
|
|
217
|
+
|
|
218
|
+
// Merge two components WITH pair emission — SUPERLEVEL sweep (descending).
|
|
219
|
+
//
|
|
220
|
+
// Symmetric to sublevel: here we sweep from the highest value downward
|
|
221
|
+
// (equivalently, we run a sublevel sweep on the negated signal). The Elder
|
|
222
|
+
// Rule is flipped: the component with the LOWER birth value (= younger in
|
|
223
|
+
// the descending sweep, born later as the water drains) dies.
|
|
224
|
+
// Persistence = dyingBirthValue − mergeHeight (positive because birth > merge).
|
|
225
|
+
const mergeSuperlevel = function ( parent, birthVal, birthIdx, a, b, h ) {
|
|
226
|
+
const ra = find( parent, a );
|
|
227
|
+
const rb = find( parent, b );
|
|
228
|
+
|
|
229
|
+
/* c8 ignore next 4 -- defensive: LEFT and RIGHT components are disjoint in 1D before this merge point p, so ra !== rb */
|
|
230
|
+
if ( ra === rb ) {
|
|
231
|
+
mergeResult.merged = false;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let loser;
|
|
235
|
+
let winner;
|
|
236
|
+
// Descending sweep: higher birthVal = older. Lower birthVal dies.
|
|
237
|
+
if ( birthVal[ ra ] < birthVal[ rb ] ) {
|
|
238
|
+
loser = ra;
|
|
239
|
+
winner = rb;
|
|
240
|
+
} else if ( birthVal[ rb ] < birthVal[ ra ] ) {
|
|
241
|
+
loser = rb;
|
|
242
|
+
winner = ra;
|
|
243
|
+
|
|
244
|
+
/* c8 ignore next 3 -- defensive: ra roots the LEFT component (bIdx in [0, p-1]), rb roots the RIGHT (bIdx in [p+1, W-1]), so bIdx[ra] < bIdx[rb] always */
|
|
245
|
+
} else if ( birthIdx[ ra ] > birthIdx[ rb ] ) {
|
|
246
|
+
loser = ra;
|
|
247
|
+
winner = rb;
|
|
248
|
+
} else {
|
|
249
|
+
loser = rb;
|
|
250
|
+
winner = ra;
|
|
251
|
+
}
|
|
252
|
+
mergeResult.diedBirthVal = birthVal[ loser ];
|
|
253
|
+
mergeResult.diedBirthIdx = birthIdx[ loser ];
|
|
254
|
+
mergeResult.persistence = birthVal[ loser ] - h;
|
|
255
|
+
mergeResult.merged = true;
|
|
256
|
+
parent[ loser ] = winner;
|
|
257
|
+
}; // mergeSuperlevel()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
261
|
+
// SECTION 2: PERSISTENCE SWEEPS
|
|
262
|
+
//
|
|
263
|
+
// Two sweep functions: sweepMin (sublevel, detects significant local minima)
|
|
264
|
+
// and sweepMax (superlevel, detects significant local maxima).
|
|
265
|
+
//
|
|
266
|
+
// Both follow the same structure — the Huber/Persistence1D algorithm:
|
|
267
|
+
//
|
|
268
|
+
// For each vertex p in sorted order:
|
|
269
|
+
// Check which of its immediate neighbours (p-1, p+1) are already
|
|
270
|
+
// processed. Four cases:
|
|
271
|
+
//
|
|
272
|
+
// NEITHER processed → p seeds a new component. Its birth value is f(p).
|
|
273
|
+
//
|
|
274
|
+
// ONE processed → p extends that neighbour's component. No pair is
|
|
275
|
+
// emitted because no two distinct components are meeting.
|
|
276
|
+
//
|
|
277
|
+
// BOTH processed → p is a merge point where two components meet. First
|
|
278
|
+
// extend p into the left component (mergeNoEmit), then merge the
|
|
279
|
+
// combined left+p component with the right component (merge with emit).
|
|
280
|
+
// The second merge produces the persistence pair.
|
|
281
|
+
//
|
|
282
|
+
// sweepMin iterates sortedIndices forward (ascending value).
|
|
283
|
+
// sweepMax iterates sortedIndices backward (descending value) — this is
|
|
284
|
+
// equivalent to sorting by −f ascending, without negating or re-sorting.
|
|
285
|
+
// The union-find arrays (ufParent, ufBirthVal, ufBirthIdx) and processed
|
|
286
|
+
// flags are cleared between the two sweeps and reused.
|
|
287
|
+
//
|
|
288
|
+
// Pairs whose persistence < threshold (eps) are filtered in-place. Surviving
|
|
289
|
+
// pairs are stored in the pre-allocated state.min*Arr / state.max*Arr typed
|
|
290
|
+
// arrays (parallel arrays instead of object arrays — zero allocation).
|
|
291
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
292
|
+
|
|
293
|
+
// Sublevel sweep — detects significant local MINIMA.
|
|
294
|
+
// A local minimum is "born" when the water level first touches it (its value
|
|
295
|
+
// is the birth). It "dies" when the rising water level connects it to an
|
|
296
|
+
// older (deeper) minimum at a local maximum between them (the merge height).
|
|
297
|
+
// Persistence = merge height − birth value = amplitude of the oscillation
|
|
298
|
+
// this minimum participates in.
|
|
299
|
+
const sweepMin = function ( state, lin, W, eps ) {
|
|
300
|
+
const sorted = state.sortedIndices;
|
|
301
|
+
const parent = state.ufParent;
|
|
302
|
+
const bVal = state.ufBirthVal;
|
|
303
|
+
const bIdx = state.ufBirthIdx;
|
|
304
|
+
const processed = state.processed;
|
|
305
|
+
|
|
306
|
+
processed.fill( 0 );
|
|
307
|
+
state.minPairCount = 0;
|
|
308
|
+
|
|
309
|
+
for ( let k = 0; k < W; k += 1 ) {
|
|
310
|
+
const p = sorted[ k ];
|
|
311
|
+
const fP = lin[ p ];
|
|
312
|
+
|
|
313
|
+
// Check which neighbours are already in the processed set
|
|
314
|
+
const leftOK = p > 0 && processed[ p - 1 ] === 1;
|
|
315
|
+
const rightOK = p < ( W - 1 ) && processed[ p + 1 ] === 1;
|
|
316
|
+
|
|
317
|
+
// Initialize this vertex as its own component in the union-find
|
|
318
|
+
parent[ p ] = p;
|
|
319
|
+
bVal[ p ] = fP;
|
|
320
|
+
bIdx[ p ] = p;
|
|
321
|
+
|
|
322
|
+
if ( !leftOK && !rightOK ) {
|
|
323
|
+
// SEED: no processed neighbours → new component born at f(p).
|
|
324
|
+
// Nothing to merge. The component will grow as neighbours are
|
|
325
|
+
// processed later in the sweep.
|
|
326
|
+
} else if ( leftOK && !rightOK ) {
|
|
327
|
+
// EXTEND LEFT: only the left neighbour is processed. p joins
|
|
328
|
+
// that component. No second component is involved, so no pair.
|
|
329
|
+
mergeNoEmitSub( parent, bVal, bIdx, p, p - 1 );
|
|
330
|
+
} else if ( !leftOK && rightOK ) {
|
|
331
|
+
// EXTEND RIGHT: symmetric to extend-left.
|
|
332
|
+
mergeNoEmitSub( parent, bVal, bIdx, p, p + 1 );
|
|
333
|
+
} else {
|
|
334
|
+
// MERGE: both neighbours are processed. Two distinct components
|
|
335
|
+
// meet at p. First extend p into the left component (no pair),
|
|
336
|
+
// then merge the combined {left ∪ p} with the right component.
|
|
337
|
+
// The second merge is where the Elder Rule applies and the
|
|
338
|
+
// dying component's pair is recorded.
|
|
339
|
+
mergeNoEmitSub( parent, bVal, bIdx, p, p - 1 );
|
|
340
|
+
mergeSublevel( parent, bVal, bIdx, p, p + 1, fP );
|
|
341
|
+
if ( mergeResult.merged && mergeResult.persistence >= eps ) {
|
|
342
|
+
const i = state.minPairCount;
|
|
343
|
+
if ( i < state.maxPairs ) {
|
|
344
|
+
state.minBirthValArr[ i ] = mergeResult.diedBirthVal;
|
|
345
|
+
state.minBirthIdxArr[ i ] = mergeResult.diedBirthIdx;
|
|
346
|
+
state.minDeathValArr[ i ] = fP;
|
|
347
|
+
state.minDeathIdxArr[ i ] = p;
|
|
348
|
+
state.minPersArr[ i ] = mergeResult.persistence;
|
|
349
|
+
state.minPairCount = i + 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
processed[ p ] = 1;
|
|
354
|
+
}
|
|
355
|
+
}; // sweepMin()
|
|
356
|
+
|
|
357
|
+
// Superlevel sweep — detects significant local MAXIMA.
|
|
358
|
+
// Symmetric to sweepMin but iterates in REVERSE through sortedIndices
|
|
359
|
+
// (descending value = the water level dropping from above). A local maximum
|
|
360
|
+
// is "born" when the draining water first exposes it. It "dies" when the
|
|
361
|
+
// water drops low enough to connect it to an older (higher) maximum.
|
|
362
|
+
// Persistence = birth value − merge height.
|
|
363
|
+
//
|
|
364
|
+
// Implementation note: instead of negating the signal and re-sorting, we
|
|
365
|
+
// reuse the same ascending sort and iterate backward. This halves the sort
|
|
366
|
+
// cost and avoids allocating a negated copy of the signal. The Elder Rule
|
|
367
|
+
// comparison is flipped (lower birth dies), and persistence is computed as
|
|
368
|
+
// birthVal − h (instead of h − birthVal) to keep it positive.
|
|
369
|
+
const sweepMax = function ( state, lin, W, eps ) {
|
|
370
|
+
const sorted = state.sortedIndices;
|
|
371
|
+
const parent = state.ufParent;
|
|
372
|
+
const bVal = state.ufBirthVal;
|
|
373
|
+
const bIdx = state.ufBirthIdx;
|
|
374
|
+
const processed = state.processed;
|
|
375
|
+
|
|
376
|
+
processed.fill( 0 );
|
|
377
|
+
state.maxPairCount = 0;
|
|
378
|
+
|
|
379
|
+
for ( let k = W - 1; k >= 0; k -= 1 ) {
|
|
380
|
+
const p = sorted[ k ];
|
|
381
|
+
const fP = lin[ p ];
|
|
382
|
+
|
|
383
|
+
const leftOK = p > 0 && processed[ p - 1 ] === 1;
|
|
384
|
+
const rightOK = p < ( W - 1 ) && processed[ p + 1 ] === 1;
|
|
385
|
+
|
|
386
|
+
parent[ p ] = p;
|
|
387
|
+
bVal[ p ] = fP;
|
|
388
|
+
bIdx[ p ] = p;
|
|
389
|
+
|
|
390
|
+
if ( !leftOK && !rightOK ) {
|
|
391
|
+
// SEED
|
|
392
|
+
} else if ( leftOK && !rightOK ) {
|
|
393
|
+
mergeNoEmitSup( parent, bVal, bIdx, p, p - 1 );
|
|
394
|
+
} else if ( !leftOK && rightOK ) {
|
|
395
|
+
mergeNoEmitSup( parent, bVal, bIdx, p, p + 1 );
|
|
396
|
+
} else {
|
|
397
|
+
// MERGE — Elder Rule flipped for superlevel
|
|
398
|
+
mergeNoEmitSup( parent, bVal, bIdx, p, p - 1 );
|
|
399
|
+
mergeSuperlevel( parent, bVal, bIdx, p, p + 1, fP );
|
|
400
|
+
if ( mergeResult.merged && mergeResult.persistence >= eps ) {
|
|
401
|
+
const i = state.maxPairCount;
|
|
402
|
+
if ( i < state.maxPairs ) {
|
|
403
|
+
state.maxBirthValArr[ i ] = mergeResult.diedBirthVal;
|
|
404
|
+
state.maxBirthIdxArr[ i ] = mergeResult.diedBirthIdx;
|
|
405
|
+
state.maxDeathValArr[ i ] = fP;
|
|
406
|
+
state.maxDeathIdxArr[ i ] = p;
|
|
407
|
+
state.maxPersArr[ i ] = mergeResult.persistence;
|
|
408
|
+
state.maxPairCount = i + 1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
processed[ p ] = 1;
|
|
413
|
+
}
|
|
414
|
+
}; // sweepMax()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
418
|
+
// SECTION 3: PAIR DIFFING
|
|
419
|
+
//
|
|
420
|
+
// The persistence diagram changes incrementally as the window slides: one
|
|
421
|
+
// sample enters on the right, one sample exits on the left. Most pairs
|
|
422
|
+
// persist between ticks (same physical samples, shifted by one position in
|
|
423
|
+
// the linearized window). Only NEWLY-APPEARED pairs should fire completion
|
|
424
|
+
// events — otherwise downstream nodes like `appraise` would see the same
|
|
425
|
+
// event re-emitted on every tick.
|
|
426
|
+
//
|
|
427
|
+
// The diffing works by comparing each current-tick pair (birthIdx, deathIdx)
|
|
428
|
+
// against the previous tick's pairs with an INDEX SHIFT correction:
|
|
429
|
+
//
|
|
430
|
+
// Previous tick's linearized index I → current tick's index (I − 1)
|
|
431
|
+
//
|
|
432
|
+
// because the oldest sample (at index 0 in the previous tick) has been
|
|
433
|
+
// evicted, and all other samples have shifted left by one position.
|
|
434
|
+
//
|
|
435
|
+
// A pair is "same" if prev.birthIdx − 1 === cur.birthIdx AND
|
|
436
|
+
// prev.deathIdx − 1 === cur.deathIdx. Pairs where either shifted index < 0
|
|
437
|
+
// have fallen off the window and are not candidates.
|
|
438
|
+
//
|
|
439
|
+
// BOUNDARY ARTIFACTS. Index-shift matching alone under-suppresses: both
|
|
440
|
+
// window edges mint pairs that are not new physical events. At the left
|
|
441
|
+
// edge, evicting the oldest sample can re-form a pair for a swing that was
|
|
442
|
+
// already reported, with different indices, so a matcher calls it "new"
|
|
443
|
+
// (confirmed on real plant data, 2026-07-20 value study: 46–78% of raw
|
|
444
|
+
// completions were such re-fires). At the right edge, a descent in progress
|
|
445
|
+
// births a provisional pair at the newest sample on every tick, re-reporting
|
|
446
|
+
// the same still-deepening swing. Emission is therefore flag-based — a pair
|
|
447
|
+
// emits exactly when its birth vertex has never been accounted — under four
|
|
448
|
+
// rules:
|
|
449
|
+
//
|
|
450
|
+
// (a) LEFT-BOUNDARY BIRTH — a pair whose birth vertex is window index 0
|
|
451
|
+
// never emits. The swing's left half is cut off by the window, so the
|
|
452
|
+
// birth can never be certified as a real extremum. This also stops
|
|
453
|
+
// the every-tick "walk" where an evicted deep swing's shoulder keeps
|
|
454
|
+
// re-forming a boundary pair as it slides out.
|
|
455
|
+
// (b) REBIRTH — a pair that inherits its death vertex from a
|
|
456
|
+
// previous-tick pair whose birth vertex was just evicted (previous
|
|
457
|
+
// birthIdx 0) is the same swing re-formed around a surviving ripple,
|
|
458
|
+
// not a new swing.
|
|
459
|
+
// (c) PER-VERTEX ACCOUNTING — each birth vertex drives at most one
|
|
460
|
+
// emission while it lives in the window. A per-ring-slot flag records
|
|
461
|
+
// vertices already accounted (emitted, or recognized as a duplicate);
|
|
462
|
+
// the flag clears when the slot is overwritten by a new sample. This
|
|
463
|
+
// catches re-pairings that (a) and (b) cannot see (e.g. a completed
|
|
464
|
+
// minimum re-pairing at a different death vertex after an elder
|
|
465
|
+
// eviction) and skips persisting pairs without index matching.
|
|
466
|
+
// (d) RIGHT-BOUNDARY BIRTH — a pair whose birth vertex is the newest
|
|
467
|
+
// sample (index W−1) is provisional: the extremum cannot be certified
|
|
468
|
+
// until its right neighbour arrives. It stays stored but unaccounted;
|
|
469
|
+
// it emits one tick after the true turning point, when the re-derived
|
|
470
|
+
// pair has birth index < W−1 and a still-clear flag.
|
|
471
|
+
//
|
|
472
|
+
// The STORED pair arrays remain the full finite-domain persistence diagram
|
|
473
|
+
// of the window (matching the GUDHI golden truth); the rules govern only
|
|
474
|
+
// which pairs surface as completion events.
|
|
475
|
+
//
|
|
476
|
+
// Deliberate consequences of these rules:
|
|
477
|
+
// - A genuine swing spanning the entire window (birth exactly at index 0
|
|
478
|
+
// when first resolvable) is never emitted — the price of (a). Size
|
|
479
|
+
// windowSize with margin over the slowest swing of interest.
|
|
480
|
+
// - Completions surface one sample after the extremum is confirmed — the
|
|
481
|
+
// price of (d), and the semantics a streaming consumer wants (no
|
|
482
|
+
// retractions of provisional events).
|
|
483
|
+
// - A swing revealed by truncation (an elder's interior ripple that flips
|
|
484
|
+
// younger after the elder's bottom evicts) still emits, once. It was
|
|
485
|
+
// never reported before, so this is not a duplicate.
|
|
486
|
+
//
|
|
487
|
+
// For each NEW pair, we select the one with the largest persistence as
|
|
488
|
+
// the completion event for this tick. If multiple new pairs appear (rare
|
|
489
|
+
// but possible when the new sample triggers a cascade of merges), only
|
|
490
|
+
// the deepest one drives the completion slots; `swingsThisTick` records
|
|
491
|
+
// the total count of genuinely new pairs.
|
|
492
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
493
|
+
|
|
494
|
+
// Diff one direction's current pairs against previous tick and fire
|
|
495
|
+
// completion events for genuinely new pairs only, applying the three
|
|
496
|
+
// eviction-artifact suppression rules documented in the section banner.
|
|
497
|
+
// curBirthIdx/curDeathIdx/curPersist/curBirthVal are the current tick's
|
|
498
|
+
// parallel arrays; prevBirthIdx/prevDeathIdx/prevCount are previous tick's.
|
|
499
|
+
// keys is state.fieldKeys.min or state.fieldKeys.max (pre-built at init) —
|
|
500
|
+
// avoids per-tick string concatenation for the four target field names.
|
|
501
|
+
// flagArr is the direction's per-ring-slot accounted flag (rule c); head is
|
|
502
|
+
// the ring's oldest-sample slot, so window index i maps to ring slot
|
|
503
|
+
// (head + i) mod W.
|
|
504
|
+
// Account one current-tick pair (guard-clause form). Applies the four
|
|
505
|
+
// suppression rules and writes the completion slots for a genuinely new
|
|
506
|
+
// pair. Returns nothing; all effects land on state and flagArr.
|
|
507
|
+
//
|
|
508
|
+
// Emission is flag-based, not match-based: a pair emits exactly when its
|
|
509
|
+
// birth vertex has never been accounted (flag clear) and no rule blocks
|
|
510
|
+
// it. Persisting pairs are skipped by their standing flag, so no explicit
|
|
511
|
+
// same-pair index matching is needed.
|
|
512
|
+
const accountPair = function ( state, W, cb, cd, birthVal, pers,
|
|
513
|
+
prevBirthIdx, prevDeathIdx, prevCount, keys, flagArr, head ) {
|
|
514
|
+
// Rule (a): left-boundary birth — the swing's left half is cut off by
|
|
515
|
+
// the window, so the extremum can never be certified. Never emits and
|
|
516
|
+
// never flags (the slot is overwritten next tick anyway).
|
|
517
|
+
if ( cb === 0 ) return;
|
|
518
|
+
|
|
519
|
+
// Rule (d): right-boundary birth — the newest sample cannot yet be
|
|
520
|
+
// certified as an extremum because its right neighbour has not arrived.
|
|
521
|
+
// The pair stays stored but unaccounted; once a confirming sample
|
|
522
|
+
// lands, the re-derived pair (birth index < W−1, flag still clear)
|
|
523
|
+
// emits through the normal path below.
|
|
524
|
+
if ( cb === ( W - 1 ) ) return;
|
|
525
|
+
|
|
526
|
+
// Rule (b): a pair inheriting its death vertex from a previous-tick
|
|
527
|
+
// pair whose birth vertex was just evicted (previous birthIdx 0) is
|
|
528
|
+
// the same swing re-formed around a surviving ripple, not a new swing.
|
|
529
|
+
let rebirth = false;
|
|
530
|
+
for ( let j = 0; j < prevCount; j += 1 ) {
|
|
531
|
+
if ( ( ( prevDeathIdx[ j ] - 1 ) === cd ) && ( prevBirthIdx[ j ] === 0 ) ) {
|
|
532
|
+
rebirth = true;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Map the birth's window index to its ring slot (avoids %).
|
|
538
|
+
let slot = head + cb;
|
|
539
|
+
if ( slot >= W ) slot -= W;
|
|
540
|
+
|
|
541
|
+
if ( rebirth || ( flagArr[ slot ] === 1 ) ) {
|
|
542
|
+
// Rules (b)/(c): a duplicate of an already-accounted swing. Mark
|
|
543
|
+
// the vertex so later re-pairings stay suppressed; emit nothing.
|
|
544
|
+
flagArr[ slot ] = 1;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Genuinely new, certified completion.
|
|
549
|
+
flagArr[ slot ] = 1;
|
|
550
|
+
state.swingsThisTick += 1;
|
|
551
|
+
if ( !state[ keys.completed ] || ( pers > state[ keys.persistence ] ) ) {
|
|
552
|
+
state[ keys.completed ] = true;
|
|
553
|
+
state[ keys.birthValue ] = birthVal;
|
|
554
|
+
state[ keys.birthLag ] = ( W - 1 ) - cb;
|
|
555
|
+
state[ keys.persistence ] = pers;
|
|
556
|
+
}
|
|
557
|
+
}; // accountPair()
|
|
558
|
+
|
|
559
|
+
const diffOneSide = function ( state, W, curCount, curBirthIdxArr, curDeathIdxArr,
|
|
560
|
+
curBirthValArr, curPersArr, prevBirthIdx, prevDeathIdx, prevCount, keys,
|
|
561
|
+
flagArr, head ) {
|
|
562
|
+
for ( let i = 0; i < curCount; i += 1 ) {
|
|
563
|
+
accountPair( state, W, curBirthIdxArr[ i ], curDeathIdxArr[ i ],
|
|
564
|
+
curBirthValArr[ i ], curPersArr[ i ],
|
|
565
|
+
prevBirthIdx, prevDeathIdx, prevCount, keys, flagArr, head );
|
|
566
|
+
}
|
|
567
|
+
}; // diffOneSide()
|
|
568
|
+
|
|
569
|
+
const diffAndEmit = function ( state, W ) {
|
|
570
|
+
const head = state.ring.head;
|
|
571
|
+
if ( state.direction === 'both' || state.direction === 'dips' ) {
|
|
572
|
+
diffOneSide( state, W, state.minPairCount,
|
|
573
|
+
state.minBirthIdxArr, state.minDeathIdxArr,
|
|
574
|
+
state.minBirthValArr, state.minPersArr,
|
|
575
|
+
state.prevMinBirthIdx, state.prevMinDeathIdx,
|
|
576
|
+
state.prevMinPairCount, state.fieldKeys.min,
|
|
577
|
+
state.minEmitFlag, head );
|
|
578
|
+
}
|
|
579
|
+
if ( state.direction === 'both' || state.direction === 'peaks' ) {
|
|
580
|
+
diffOneSide( state, W, state.maxPairCount,
|
|
581
|
+
state.maxBirthIdxArr, state.maxDeathIdxArr,
|
|
582
|
+
state.maxBirthValArr, state.maxPersArr,
|
|
583
|
+
state.prevMaxBirthIdx, state.prevMaxDeathIdx,
|
|
584
|
+
state.prevMaxPairCount, state.fieldKeys.max,
|
|
585
|
+
state.maxEmitFlag, head );
|
|
586
|
+
}
|
|
587
|
+
if ( state.dipCompleted ) state.emitted += 1;
|
|
588
|
+
if ( state.peakCompleted ) state.emitted += 1;
|
|
589
|
+
}; // diffAndEmit()
|
|
590
|
+
|
|
591
|
+
// Copy current tick's pair indices to previous-tick storage for the next
|
|
592
|
+
// tick's diff comparison. Only indices are needed (not values or persistence)
|
|
593
|
+
// because the diff compares by position, not by magnitude.
|
|
594
|
+
const copyToPrev = function ( state ) {
|
|
595
|
+
for ( let i = 0; i < state.minPairCount; i += 1 ) {
|
|
596
|
+
state.prevMinBirthIdx[ i ] = state.minBirthIdxArr[ i ];
|
|
597
|
+
state.prevMinDeathIdx[ i ] = state.minDeathIdxArr[ i ];
|
|
598
|
+
}
|
|
599
|
+
state.prevMinPairCount = state.minPairCount;
|
|
600
|
+
|
|
601
|
+
for ( let i = 0; i < state.maxPairCount; i += 1 ) {
|
|
602
|
+
state.prevMaxBirthIdx[ i ] = state.maxBirthIdxArr[ i ];
|
|
603
|
+
state.prevMaxDeathIdx[ i ] = state.maxDeathIdxArr[ i ];
|
|
604
|
+
}
|
|
605
|
+
state.prevMaxPairCount = state.maxPairCount;
|
|
606
|
+
}; // copyToPrev()
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
610
|
+
// SECTION 4: MAIN UPDATE
|
|
611
|
+
//
|
|
612
|
+
// The per-message entry point. Called by the wiring layer for every sample.
|
|
613
|
+
//
|
|
614
|
+
// Flow:
|
|
615
|
+
// Resolve the adaptive threshold from the tunable, applying the absolute floor.
|
|
616
|
+
// The try/catch pattern with log suppression follows the threshold node's
|
|
617
|
+
// convention for handling tunable errors gracefully.
|
|
618
|
+
const resolveThreshold = function ( state, msg ) {
|
|
619
|
+
try {
|
|
620
|
+
state.currentThreshold = state.thresholdFn( msg );
|
|
621
|
+
if ( state.tunableErrorLogged ) {
|
|
622
|
+
state.tunableErrorLogged = false;
|
|
623
|
+
}
|
|
624
|
+
} catch ( error ) {
|
|
625
|
+
if ( !state.tunableErrorLogged ) {
|
|
626
|
+
state.tunableErrorLogged = true;
|
|
627
|
+
const errMsg = 'WinkComposer/' + state.nodeType + ': tunable threw: ' + error.message;
|
|
628
|
+
console.error( errMsg );
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const epsBase = state.currentThreshold;
|
|
632
|
+
// Apply the absolute floor: effective ε = max(resolved, minAbsoluteThreshold).
|
|
633
|
+
// Required for quiet signals where adaptive σ drops below the measurement
|
|
634
|
+
// noise floor — empirically validated on WiFi RSSI (see feasibility study).
|
|
635
|
+
return ( epsBase < state.minAbsoluteThreshold ) ? state.minAbsoluteThreshold : epsBase;
|
|
636
|
+
}; // resolveThreshold()
|
|
637
|
+
|
|
638
|
+
// 1. Guard (disable / pause)
|
|
639
|
+
// 2. Reset per-tick output slots (output scrubbing)
|
|
640
|
+
// 3. Extract and validate input
|
|
641
|
+
// 4. Resolve adaptive threshold
|
|
642
|
+
// 5. Push sample to ring buffer
|
|
643
|
+
// 6. Wait for warmup (window not yet full)
|
|
644
|
+
// 7. Linearize the circular ring buffer into a contiguous array
|
|
645
|
+
// 8. Sort sample indices by value (ascending)
|
|
646
|
+
// 9. Run sublevel sweep for minima (if direction allows)
|
|
647
|
+
// 10. Run superlevel sweep for maxima (if direction allows)
|
|
648
|
+
// 11. Diff current diagram against previous tick's diagram
|
|
649
|
+
// 12. Copy current pairs to previous-tick storage
|
|
650
|
+
// 13. Update diagnostics
|
|
651
|
+
//
|
|
652
|
+
// Steps 7–12 are the "batch per window" core. Their combined cost is
|
|
653
|
+
// O(W log W) dominated by the sort, with O(W) for the sweeps and O(P²)
|
|
654
|
+
// for the diff where P is the pair count (typically 5–20, negligible).
|
|
655
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
656
|
+
|
|
657
|
+
const update = function ( state, msg ) {
|
|
658
|
+
// ── 1. Control guard ─────────────────────────────────────
|
|
659
|
+
if ( state.disable || state.pause ) return state;
|
|
660
|
+
|
|
661
|
+
// ── 2. Reset per-tick completion slots ───────────────────
|
|
662
|
+
// Output scrubbing: downstream must never read stale completion data
|
|
663
|
+
// from a previous tick. Set all slots to their "no event" state. The
|
|
664
|
+
// momentsDigest node establishes this convention — non-event ticks
|
|
665
|
+
// publish `undefined` for detail fields, not stale values.
|
|
666
|
+
state.dipCompleted = false;
|
|
667
|
+
state.peakCompleted = false;
|
|
668
|
+
state.dipValue = NaN;
|
|
669
|
+
state.peakValue = NaN;
|
|
670
|
+
state.dipLag = 0;
|
|
671
|
+
state.peakLag = 0;
|
|
672
|
+
state.dipSize = NaN;
|
|
673
|
+
state.peakSize = NaN;
|
|
674
|
+
state.swingsThisTick = 0;
|
|
675
|
+
|
|
676
|
+
// ── 3. Extract and validate input ────────────────────────
|
|
677
|
+
const xVal = msg[ state.x ];
|
|
678
|
+
state.inputValidationFailed = false;
|
|
679
|
+
|
|
680
|
+
if ( !Number.isFinite( xVal ) ) {
|
|
681
|
+
state.inputValidationFailed = true;
|
|
682
|
+
return state;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ── 4. Resolve adaptive threshold ────────────────────────
|
|
686
|
+
const eps = resolveThreshold( state, msg );
|
|
687
|
+
|
|
688
|
+
// ── 5. Push to ring buffer ───────────────────────────────
|
|
689
|
+
// The slot being overwritten is the ring's head (next write position =
|
|
690
|
+
// oldest sample). Clear its accounted-emission flags so the incoming
|
|
691
|
+
// sample starts life unflagged (rule c bookkeeping).
|
|
692
|
+
const evictSlot = state.ring.head;
|
|
693
|
+
push( state.ring, xVal );
|
|
694
|
+
state.minEmitFlag[ evictSlot ] = 0;
|
|
695
|
+
state.maxEmitFlag[ evictSlot ] = 0;
|
|
696
|
+
state.received += 1;
|
|
697
|
+
|
|
698
|
+
// ── 6. Warmup gate ───────────────────────────────────────
|
|
699
|
+
// Do not run the sweep until the ring buffer is full. During warmup,
|
|
700
|
+
// completion slots remain in their "no event" state and publishTo
|
|
701
|
+
// will return without writing any fields to the message.
|
|
702
|
+
if ( isNotFull( state.ring ) ) return state;
|
|
703
|
+
|
|
704
|
+
const W = state.windowSize;
|
|
705
|
+
|
|
706
|
+
// ── 7. Linearize ring → contiguous array ─────────────────
|
|
707
|
+
// The ring buffer is circular: ring.head points to the next write
|
|
708
|
+
// position, which is also the OLDEST sample (it will be overwritten
|
|
709
|
+
// next tick). We need a contiguous array where index 0 = oldest and
|
|
710
|
+
// index W−1 = newest, because the sort and sweep operate on contiguous
|
|
711
|
+
// indices.
|
|
712
|
+
//
|
|
713
|
+
// Manual indexed loops avoid subarray()'s per-call view-object
|
|
714
|
+
// allocation. At W ≤ 256 the two loops iterate at most W elements
|
|
715
|
+
// total on typed arrays — V8 vectorizes trivially, zero allocation.
|
|
716
|
+
const buf = state.ring.buffer;
|
|
717
|
+
const head = state.ring.head;
|
|
718
|
+
const lin = state.linear;
|
|
719
|
+
const firstLen = W - head;
|
|
720
|
+
for ( let i = 0; i < firstLen; i += 1 ) lin[ i ] = buf[ head + i ];
|
|
721
|
+
for ( let i = 0; i < head; i += 1 ) lin[ firstLen + i ] = buf[ i ];
|
|
722
|
+
|
|
723
|
+
// ── 8. Sort indices by value ─────────────────────────────
|
|
724
|
+
// Initialize sortedIndices to [0, 1, 2, ..., W−1], then sort by
|
|
725
|
+
// linear[i] ascending. V8's Timsort detects existing runs — since the
|
|
726
|
+
// window changes by only one sample per tick, most of the sorted order
|
|
727
|
+
// is preserved, and Timsort approaches O(W) in practice.
|
|
728
|
+
//
|
|
729
|
+
// Tie-breaking by index (a − b) ensures deterministic results and
|
|
730
|
+
// matches Persistence1D's convention: earlier samples process first
|
|
731
|
+
// when values are equal. Comparator is pre-built in init to avoid
|
|
732
|
+
// per-tick closure allocation.
|
|
733
|
+
for ( let i = 0; i < W; i += 1 ) state.sortedIndices[ i ] = i;
|
|
734
|
+
state.sortedIndices.sort( state.compareIndices );
|
|
735
|
+
|
|
736
|
+
// ── 9. Sweep for minima ──────────────────────────────────
|
|
737
|
+
if ( state.direction === 'both' || state.direction === 'dips' ) {
|
|
738
|
+
sweepMin( state, lin, W, eps );
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ── 10. Sweep for maxima ─────────────────────────────────
|
|
742
|
+
// Uses the SAME sortedIndices (iterated backward) and resets the same
|
|
743
|
+
// uf* and processed arrays. No re-sort needed.
|
|
744
|
+
if ( state.direction === 'both' || state.direction === 'peaks' ) {
|
|
745
|
+
sweepMax( state, lin, W, eps );
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ── 11. Diff and emit ────────────────────────────────────
|
|
749
|
+
diffAndEmit( state, W );
|
|
750
|
+
|
|
751
|
+
// ── 12. Copy current → prev ──────────────────────────────
|
|
752
|
+
copyToPrev( state );
|
|
753
|
+
|
|
754
|
+
// ── 13. Update diagnostic ────────────────────────────────
|
|
755
|
+
// Cumulative swings per received sample. Can exceed 1.0 in
|
|
756
|
+
// direction='both' when ticks complete both a dip and a peak.
|
|
757
|
+
// The ternary's `: 0` branch is unreachable — received was incremented
|
|
758
|
+
// earlier in this function (step 3) and is therefore ≥ 1 here.
|
|
759
|
+
/* c8 ignore next 3 -- defensive: received ≥ 1 by line 561 */
|
|
760
|
+
state.swingRate = ( state.received > 0 ) ?
|
|
761
|
+
state.emitted / state.received :
|
|
762
|
+
0;
|
|
763
|
+
|
|
764
|
+
return state;
|
|
765
|
+
}; // update()
|
|
766
|
+
|
|
767
|
+
export default update;
|