-
-
Notifications
You must be signed in to change notification settings - Fork 0
[limen GEN-a-organvm-metasystem-master-test-coverage-0620] Raise test coverage in a-organvm/metasystem-master #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,6 +112,39 @@ export function calculateWeight( | |
| return Math.max(0.001, Math.min(1, weight)); // Clamp to avoid zero weights | ||
| } | ||
|
|
||
| function calculateConsensusWeights( | ||
| inputs: AudienceInput[], | ||
| config: WeightingConfig | ||
| ): number[] { | ||
| if (inputs.length <= 1) return inputs.map(() => 1.0); | ||
|
|
||
| const sorted = inputs | ||
| .map((input, index) => ({ index, value: input.value })) | ||
| .sort((a, b) => a.value - b.value); | ||
| const weights = new Array<number>(inputs.length); | ||
| let left = 0; | ||
| let right = 0; | ||
|
|
||
| for (let i = 0; i < sorted.length; i++) { | ||
| const value = sorted[i].value; | ||
|
|
||
| while (value - sorted[left].value > config.clusterThreshold) { | ||
| left++; | ||
| } | ||
|
|
||
| while ( | ||
| right + 1 < sorted.length && | ||
| sorted[right + 1].value - value <= config.clusterThreshold | ||
| ) { | ||
| right++; | ||
| } | ||
|
|
||
| weights[sorted[i].index] = (right - left) / (inputs.length - 1); | ||
| } | ||
|
|
||
| return weights; | ||
| } | ||
|
|
||
| // ============================================================================= | ||
| // WEIGHTED INPUT GENERATION | ||
| // ============================================================================= | ||
|
|
@@ -125,11 +158,12 @@ export function weightInputs( | |
| config: WeightingConfig = DEFAULT_WEIGHTING_CONFIG | ||
| ): WeightedInput[] { | ||
| const currentTime = Date.now(); | ||
| const consensusWeights = calculateConsensusWeights(inputs, config); | ||
|
|
||
| return inputs.map(input => { | ||
| return inputs.map((input, index) => { | ||
| const spatialWeight = calculateSpatialWeight(input.location, stagePosition, config); | ||
| const temporalWeight = calculateTemporalWeight(input.timestamp, currentTime, config); | ||
| const consensusWeight = calculateConsensusWeight(input, inputs, config); | ||
| const consensusWeight = consensusWeights[index]; | ||
|
|
||
| const weight = | ||
| config.spatialAlpha * spatialWeight + | ||
|
|
@@ -186,25 +220,46 @@ export function standardDeviation(inputs: WeightedInput[]): number { | |
| } | ||
|
|
||
| /** | ||
| * Remove outliers using z-score method. | ||
| * Remove outliers using robust modified z-score. | ||
| */ | ||
| export function removeOutliers( | ||
| inputs: WeightedInput[], | ||
| threshold: number = 2.5 | ||
| ): WeightedInput[] { | ||
| if (inputs.length < 4) return inputs; | ||
|
|
||
| const mean = weightedMean(inputs); | ||
| const std = standardDeviation(inputs); | ||
|
|
||
| if (std < 0.001) return inputs; // No variance | ||
|
|
||
|
|
||
| const medianValue = median(inputs.map(input => input.value)); | ||
| const deviations = inputs.map(input => Math.abs(input.value - medianValue)); | ||
| const medianAbsoluteDeviation = median(deviations); | ||
|
|
||
| if (medianAbsoluteDeviation < 0.001) { | ||
| const hasVariance = deviations.some(deviation => deviation > 0.001); | ||
| if (!hasVariance) return inputs; | ||
|
|
||
| return inputs.filter(input => Math.abs(input.value - medianValue) <= 0.15); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the median absolute deviation collapses to zero, this branch ignores the caller-provided Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| return inputs.filter(input => { | ||
| const zScore = Math.abs((input.value - mean) / std); | ||
| return zScore <= threshold; | ||
| const modifiedZScore = ( | ||
| 0.6745 * Math.abs(input.value - medianValue) | ||
| ) / medianAbsoluteDeviation; | ||
| return modifiedZScore <= threshold; | ||
| }); | ||
| } | ||
|
|
||
| function median(values: number[]): number { | ||
| if (values.length === 0) return 0; | ||
|
|
||
| const sorted = [...values].sort((a, b) => a - b); | ||
| const middle = Math.floor(sorted.length / 2); | ||
|
|
||
| if (sorted.length % 2 === 1) { | ||
| return sorted[middle]; | ||
| } | ||
|
|
||
| return (sorted[middle - 1] + sorted[middle]) / 2; | ||
| } | ||
|
|
||
| /** | ||
| * Apply exponential smoothing to reduce jitter. | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,269 @@ | ||
| /** | ||
| * OSC Bridge Tests for Omni-Dromenon-Engine | ||
| */ | ||
|
|
||
| import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; | ||
| import { createBus } from '../src/bus/parameter-bus.js'; | ||
| import { createOSCBridge } from '../src/osc/osc-bridge.js'; | ||
| import { ConsensusMode, type ConsensusResult } from '../src/types/index.js'; | ||
|
|
||
| const oscMock = vi.hoisted(() => { | ||
| class MockUDPPort { | ||
| options: Record<string, unknown>; | ||
| sent: unknown[] = []; | ||
| handlers = new Map<string, Array<(...args: unknown[]) => void>>(); | ||
| open = vi.fn(() => { | ||
| this.emit('ready'); | ||
| }); | ||
| close = vi.fn(); | ||
| send = vi.fn((packet: unknown) => { | ||
| this.sent.push(packet); | ||
| }); | ||
|
|
||
| constructor(options: Record<string, unknown>) { | ||
| this.options = options; | ||
| } | ||
|
|
||
| on(event: string, handler: (...args: unknown[]) => void): this { | ||
| const handlers = this.handlers.get(event) ?? []; | ||
| handlers.push(handler); | ||
| this.handlers.set(event, handlers); | ||
| return this; | ||
| } | ||
|
|
||
| emit(event: string, ...args: unknown[]): void { | ||
| for (const handler of this.handlers.get(event) ?? []) { | ||
| handler(...args); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const instances: MockUDPPort[] = []; | ||
| const UDPPort = vi.fn(function MockUDPPortConstructor( | ||
| this: unknown, | ||
| options: Record<string, unknown> | ||
| ) { | ||
| const port = new MockUDPPort(options); | ||
| instances.push(port); | ||
| return port; | ||
| }); | ||
| const timeTag = vi.fn((time: number) => ({ time })); | ||
|
|
||
| return { instances, UDPPort, timeTag }; | ||
| }); | ||
|
|
||
| vi.mock('osc', () => ({ | ||
| default: { | ||
| UDPPort: oscMock.UDPPort, | ||
| timeTag: oscMock.timeTag, | ||
| }, | ||
| })); | ||
|
|
||
| function consensus(parameter: string, value: number): ConsensusResult { | ||
| return { | ||
| parameter, | ||
| value, | ||
| confidence: 0.9, | ||
| inputCount: 4, | ||
| timestamp: 123, | ||
| mode: ConsensusMode.WEIGHTED_AVERAGE, | ||
| rawMean: value, | ||
| weightedMean: value, | ||
| standardDeviation: 0.05, | ||
| participationRate: 0.8, | ||
| }; | ||
| } | ||
|
|
||
| function latestPort() { | ||
| return oscMock.instances.at(-1); | ||
| } | ||
|
|
||
| describe('OSCBridge', () => { | ||
| let logSpy: ReturnType<typeof vi.spyOn>; | ||
| let errorSpy: ReturnType<typeof vi.spyOn>; | ||
|
|
||
| beforeEach(() => { | ||
| oscMock.instances.length = 0; | ||
| oscMock.UDPPort.mockClear(); | ||
| oscMock.timeTag.mockClear(); | ||
| logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); | ||
| errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| logSpy.mockRestore(); | ||
| errorSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('opens a configured UDP port and tracks connection state', async () => { | ||
| const bridge = createOSCBridge({ | ||
| localPort: 9101, | ||
| remoteHost: '127.0.0.2', | ||
| remotePort: 9102, | ||
| addressPrefix: '/test', | ||
| enabled: true, | ||
| }); | ||
| const connected = vi.fn(); | ||
| bridge.on('connected', connected); | ||
|
|
||
| await bridge.connect(); | ||
|
|
||
| expect(oscMock.UDPPort).toHaveBeenCalledWith({ | ||
| localAddress: '0.0.0.0', | ||
| localPort: 9101, | ||
| remoteAddress: '127.0.0.2', | ||
| remotePort: 9102, | ||
| metadata: true, | ||
| }); | ||
| expect(bridge.isConnected()).toBe(true); | ||
| expect(connected).toHaveBeenCalledOnce(); | ||
|
|
||
| const port = latestPort(); | ||
| bridge.disconnect(); | ||
|
|
||
| expect(port?.close).toHaveBeenCalledOnce(); | ||
| expect(bridge.isConnected()).toBe(false); | ||
| }); | ||
|
|
||
| it('does not create a UDP port when disabled', async () => { | ||
| const bridge = createOSCBridge({ enabled: false }); | ||
|
|
||
| await bridge.connect(); | ||
|
|
||
| expect(oscMock.UDPPort).not.toHaveBeenCalled(); | ||
| expect(bridge.isConnected()).toBe(false); | ||
| }); | ||
|
|
||
| it('sends parameter updates with the configured address prefix', async () => { | ||
| const bridge = createOSCBridge({ addressPrefix: '/test' }); | ||
|
|
||
| await bridge.connect(); | ||
| bridge.sendParameter('mood', 0.75); | ||
|
|
||
| const port = latestPort(); | ||
| expect(port?.send).toHaveBeenCalledWith({ | ||
| address: '/test/mood', | ||
| args: [{ type: 'f', value: 0.75 }], | ||
| }); | ||
| expect(bridge.getStats()).toEqual({ connected: true, messageCount: 1 }); | ||
| }); | ||
|
|
||
| it('does not send or count messages before connecting', () => { | ||
| const bridge = createOSCBridge({ addressPrefix: '/test' }); | ||
|
|
||
| bridge.sendParameter('mood', 0.75); | ||
|
|
||
| expect(latestPort()).toBeUndefined(); | ||
| expect(bridge.getStats()).toEqual({ connected: false, messageCount: 0 }); | ||
| }); | ||
|
|
||
| it('formats OSC bundles with typed arguments and timetags', async () => { | ||
| const bridge = createOSCBridge({ addressPrefix: '/test' }); | ||
| const blob = Buffer.from([1, 2, 3]); | ||
|
|
||
| await bridge.connect(); | ||
| bridge.sendBundle( | ||
| [ | ||
| { | ||
| address: '/test/mood', | ||
| args: [0.6, 'bright', true, false, blob], | ||
| }, | ||
| ], | ||
| 456 | ||
| ); | ||
|
|
||
| expect(oscMock.timeTag).toHaveBeenCalledWith(456); | ||
| expect(latestPort()?.send).toHaveBeenCalledWith({ | ||
| timeTag: { time: 456 }, | ||
| packets: [ | ||
| { | ||
| address: '/test/mood', | ||
| args: [ | ||
| { type: 'f', value: 0.6 }, | ||
| { type: 's', value: 'bright' }, | ||
| { type: 'T' }, | ||
| { type: 'F' }, | ||
| { type: 'b', value: blob }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
| expect(bridge.getStats().messageCount).toBe(1); | ||
| }); | ||
|
|
||
| it('forwards bus consensus updates and snapshots to OSC', async () => { | ||
| const bus = createBus(); | ||
| const bridge = createOSCBridge({ addressPrefix: '/test' }); | ||
|
|
||
| bridge.attachToBus(bus); | ||
| await bridge.connect(); | ||
| bus.publishConsensus(consensus('tempo', 0.42)); | ||
| bus.publishSnapshot({ | ||
| sessionId: 'session-1', | ||
| timestamp: 124, | ||
| totalParticipants: 5, | ||
| activeParticipants: 4, | ||
| results: new Map([ | ||
| ['mood', consensus('mood', 0.7)], | ||
| ['density', consensus('density', 0.2)], | ||
| ]), | ||
| }); | ||
|
|
||
| const port = latestPort(); | ||
| expect(port?.send).toHaveBeenNthCalledWith(1, { | ||
| address: '/test/tempo', | ||
| args: [{ type: 'f', value: 0.42 }], | ||
| }); | ||
| expect(port?.send).toHaveBeenNthCalledWith(2, { | ||
| timeTag: { time: 0 }, | ||
| packets: [ | ||
| { | ||
| address: '/test/mood', | ||
| args: [{ type: 'f', value: 0.7 }], | ||
| }, | ||
| { | ||
| address: '/test/density', | ||
| args: [{ type: 'f', value: 0.2 }], | ||
| }, | ||
| ], | ||
| }); | ||
| expect(bridge.getStats().messageCount).toBe(3); | ||
| }); | ||
|
|
||
| it('emits parsed incoming messages and answers ping', async () => { | ||
| const bridge = createOSCBridge({ addressPrefix: '/test' }); | ||
| const received = vi.fn(); | ||
| bridge.on('message', received); | ||
|
|
||
| await bridge.connect(); | ||
| const port = latestPort(); | ||
| port?.emit('message', { | ||
| address: '/test/tempo', | ||
| args: [{ type: 'f', value: 0.33 }], | ||
| }); | ||
| port?.emit('message', { | ||
| address: '/ignored/tempo', | ||
| args: [{ type: 'f', value: 0.99 }], | ||
| }); | ||
| port?.emit('message', { | ||
| address: '/test/ping', | ||
| args: [], | ||
| }); | ||
|
|
||
| expect(received).toHaveBeenCalledTimes(2); | ||
| expect(received).toHaveBeenNthCalledWith(1, { | ||
| address: '/test/tempo', | ||
| parameter: 'tempo', | ||
| args: [0.33], | ||
| }); | ||
| expect(received).toHaveBeenNthCalledWith(2, { | ||
| address: '/test/ping', | ||
| parameter: 'ping', | ||
| args: [], | ||
| }); | ||
| expect(port?.send).toHaveBeenCalledWith({ | ||
| address: '/test/pong', | ||
| args: [{ type: 'i', value: expect.any(Number) }], | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This median/MAD calculation ignores the
weightfield even thoughcomputeConsensuspasses in inputs that were already weighted by spatial proximity, recency, and agreement. In a live window where several stale or far-away low-weight values outnumber a smaller set of recent/front-row high-weight values, the unweighted median can classify the high-weight values as outliers and remove them beforeweightedMean, effectively overriding the weighting model that is supposed to give those inputs more influence.Useful? React with 👍 / 👎.