git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
638 lines
19 KiB
Swift
638 lines
19 KiB
Swift
//
|
|
// RuvectorWasm.swift
|
|
// Privacy-Preserving On-Device AI for iOS
|
|
//
|
|
// Uses WasmKit to run Ruvector WASM directly on iOS
|
|
// Minimum iOS: 15.0 (WasmKit requirement)
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Core Types
|
|
|
|
/// Distance metric for vector similarity
|
|
public enum DistanceMetric: UInt8 {
|
|
case euclidean = 0
|
|
case cosine = 1
|
|
case manhattan = 2
|
|
case dotProduct = 3
|
|
}
|
|
|
|
/// Quantization mode for memory optimization
|
|
public enum QuantizationMode: UInt8 {
|
|
case none = 0
|
|
case scalar = 1 // 4x compression
|
|
case binary = 2 // 32x compression
|
|
case product = 3 // Variable compression
|
|
}
|
|
|
|
/// Search result with vector ID and distance
|
|
public struct SearchResult: Identifiable {
|
|
public let id: UInt64
|
|
public let distance: Float
|
|
}
|
|
|
|
// MARK: - Health Learning Types
|
|
|
|
/// Health metric types (privacy-preserving)
|
|
public enum HealthMetricType: UInt8 {
|
|
case heartRate = 0
|
|
case steps = 1
|
|
case sleep = 2
|
|
case activeEnergy = 3
|
|
case exerciseMinutes = 4
|
|
case standHours = 5
|
|
case distance = 6
|
|
case flightsClimbed = 7
|
|
case mindfulness = 8
|
|
case respiratoryRate = 9
|
|
case bloodOxygen = 10
|
|
case hrv = 11
|
|
}
|
|
|
|
/// Health state for learning (no actual values stored)
|
|
public struct HealthState {
|
|
public let metric: HealthMetricType
|
|
public let valueBucket: UInt8 // 0-9 normalized
|
|
public let hour: UInt8
|
|
public let dayOfWeek: UInt8
|
|
|
|
public init(metric: HealthMetricType, valueBucket: UInt8, hour: UInt8, dayOfWeek: UInt8) {
|
|
self.metric = metric
|
|
self.valueBucket = min(valueBucket, 9)
|
|
self.hour = min(hour, 23)
|
|
self.dayOfWeek = min(dayOfWeek, 6)
|
|
}
|
|
}
|
|
|
|
// MARK: - Location Learning Types
|
|
|
|
/// Location categories (no coordinates stored)
|
|
public enum LocationCategory: UInt8 {
|
|
case home = 0
|
|
case work = 1
|
|
case gym = 2
|
|
case dining = 3
|
|
case shopping = 4
|
|
case transit = 5
|
|
case outdoor = 6
|
|
case entertainment = 7
|
|
case healthcare = 8
|
|
case education = 9
|
|
case unknown = 10
|
|
}
|
|
|
|
/// Location state for learning
|
|
public struct LocationState {
|
|
public let category: LocationCategory
|
|
public let hour: UInt8
|
|
public let dayOfWeek: UInt8
|
|
public let durationMinutes: UInt16
|
|
|
|
public init(category: LocationCategory, hour: UInt8, dayOfWeek: UInt8, durationMinutes: UInt16) {
|
|
self.category = category
|
|
self.hour = min(hour, 23)
|
|
self.dayOfWeek = min(dayOfWeek, 6)
|
|
self.durationMinutes = durationMinutes
|
|
}
|
|
}
|
|
|
|
// MARK: - Communication Learning Types
|
|
|
|
/// Communication event types
|
|
public enum CommEventType: UInt8 {
|
|
case callIncoming = 0
|
|
case callOutgoing = 1
|
|
case messageReceived = 2
|
|
case messageSent = 3
|
|
case emailReceived = 4
|
|
case emailSent = 5
|
|
case notification = 6
|
|
}
|
|
|
|
/// Communication state
|
|
public struct CommState {
|
|
public let eventType: CommEventType
|
|
public let hour: UInt8
|
|
public let dayOfWeek: UInt8
|
|
public let responseTimeBucket: UInt8 // 0-9 normalized
|
|
|
|
public init(eventType: CommEventType, hour: UInt8, dayOfWeek: UInt8, responseTimeBucket: UInt8) {
|
|
self.eventType = eventType
|
|
self.hour = min(hour, 23)
|
|
self.dayOfWeek = min(dayOfWeek, 6)
|
|
self.responseTimeBucket = min(responseTimeBucket, 9)
|
|
}
|
|
}
|
|
|
|
// MARK: - Calendar Learning Types
|
|
|
|
/// Calendar event types
|
|
public enum CalendarEventType: UInt8 {
|
|
case meeting = 0
|
|
case focusTime = 1
|
|
case personal = 2
|
|
case travel = 3
|
|
case breakTime = 4
|
|
case exercise = 5
|
|
case social = 6
|
|
case deadline = 7
|
|
}
|
|
|
|
/// Calendar event for learning
|
|
public struct CalendarEvent {
|
|
public let eventType: CalendarEventType
|
|
public let startHour: UInt8
|
|
public let durationMinutes: UInt16
|
|
public let dayOfWeek: UInt8
|
|
public let isRecurring: Bool
|
|
public let hasAttendees: Bool
|
|
|
|
public init(eventType: CalendarEventType, startHour: UInt8, durationMinutes: UInt16,
|
|
dayOfWeek: UInt8, isRecurring: Bool, hasAttendees: Bool) {
|
|
self.eventType = eventType
|
|
self.startHour = min(startHour, 23)
|
|
self.durationMinutes = durationMinutes
|
|
self.dayOfWeek = min(dayOfWeek, 6)
|
|
self.isRecurring = isRecurring
|
|
self.hasAttendees = hasAttendees
|
|
}
|
|
}
|
|
|
|
/// Time slot pattern
|
|
public struct TimeSlotPattern {
|
|
public let busyProbability: Float
|
|
public let avgMeetingDuration: Float
|
|
public let focusScore: Float
|
|
public let eventCount: UInt32
|
|
}
|
|
|
|
/// Focus time suggestion
|
|
public struct FocusTimeSuggestion: Identifiable {
|
|
public var id: String { "\(day)-\(startHour)" }
|
|
public let day: UInt8
|
|
public let startHour: UInt8
|
|
public let score: Float
|
|
}
|
|
|
|
// MARK: - App Usage Learning Types
|
|
|
|
/// App categories
|
|
public enum AppCategory: UInt8 {
|
|
case social = 0
|
|
case productivity = 1
|
|
case entertainment = 2
|
|
case news = 3
|
|
case communication = 4
|
|
case health = 5
|
|
case navigation = 6
|
|
case shopping = 7
|
|
case gaming = 8
|
|
case education = 9
|
|
case finance = 10
|
|
case utilities = 11
|
|
}
|
|
|
|
/// App usage session
|
|
public struct AppUsageSession {
|
|
public let category: AppCategory
|
|
public let durationSeconds: UInt32
|
|
public let hour: UInt8
|
|
public let dayOfWeek: UInt8
|
|
public let isActiveUse: Bool
|
|
|
|
public init(category: AppCategory, durationSeconds: UInt32, hour: UInt8,
|
|
dayOfWeek: UInt8, isActiveUse: Bool) {
|
|
self.category = category
|
|
self.durationSeconds = durationSeconds
|
|
self.hour = min(hour, 23)
|
|
self.dayOfWeek = min(dayOfWeek, 6)
|
|
self.isActiveUse = isActiveUse
|
|
}
|
|
}
|
|
|
|
/// Screen time summary
|
|
public struct ScreenTimeSummary {
|
|
public let totalMinutes: Float
|
|
public let topCategory: AppCategory
|
|
public let byCategory: [AppCategory: Float]
|
|
}
|
|
|
|
/// Wellbeing insight
|
|
public struct WellbeingInsight: Identifiable {
|
|
public var id: String { category }
|
|
public let category: String
|
|
public let message: String
|
|
public let score: Float
|
|
}
|
|
|
|
// MARK: - iOS Context & Recommendations
|
|
|
|
/// Device context for recommendations
|
|
public struct IOSContext {
|
|
public let hour: UInt8
|
|
public let dayOfWeek: UInt8
|
|
public let isWeekend: Bool
|
|
public let batteryLevel: UInt8 // 0-100
|
|
public let networkType: UInt8 // 0=none, 1=wifi, 2=cellular
|
|
public let locationCategory: LocationCategory
|
|
public let recentAppCategory: AppCategory
|
|
public let activityLevel: UInt8 // 0-10
|
|
public let healthScore: Float // 0-1
|
|
|
|
public init(hour: UInt8, dayOfWeek: UInt8, batteryLevel: UInt8 = 100,
|
|
networkType: UInt8 = 1, locationCategory: LocationCategory = .unknown,
|
|
recentAppCategory: AppCategory = .utilities, activityLevel: UInt8 = 5,
|
|
healthScore: Float = 0.5) {
|
|
self.hour = min(hour, 23)
|
|
self.dayOfWeek = min(dayOfWeek, 6)
|
|
self.isWeekend = dayOfWeek == 0 || dayOfWeek == 6
|
|
self.batteryLevel = min(batteryLevel, 100)
|
|
self.networkType = min(networkType, 2)
|
|
self.locationCategory = locationCategory
|
|
self.recentAppCategory = recentAppCategory
|
|
self.activityLevel = min(activityLevel, 10)
|
|
self.healthScore = min(max(healthScore, 0), 1)
|
|
}
|
|
}
|
|
|
|
/// Activity suggestion
|
|
public struct ActivitySuggestion: Identifiable {
|
|
public var id: String { category }
|
|
public let category: String
|
|
public let confidence: Float
|
|
public let reason: String
|
|
}
|
|
|
|
/// Context-aware recommendations
|
|
public struct ContextRecommendations {
|
|
public let suggestedAppCategory: AppCategory
|
|
public let focusScore: Float
|
|
public let activitySuggestions: [ActivitySuggestion]
|
|
public let optimalNotificationTime: Bool
|
|
}
|
|
|
|
// MARK: - WASM Runtime Error
|
|
|
|
/// WASM runtime errors
|
|
public enum RuvectorError: Error, LocalizedError {
|
|
case wasmNotLoaded
|
|
case initializationFailed(String)
|
|
case memoryAllocationFailed
|
|
case invalidInput(String)
|
|
case serializationFailed
|
|
case deserializationFailed
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .wasmNotLoaded:
|
|
return "WASM module not loaded"
|
|
case .initializationFailed(let msg):
|
|
return "Initialization failed: \(msg)"
|
|
case .memoryAllocationFailed:
|
|
return "Memory allocation failed"
|
|
case .invalidInput(let msg):
|
|
return "Invalid input: \(msg)"
|
|
case .serializationFailed:
|
|
return "Serialization failed"
|
|
case .deserializationFailed:
|
|
return "Deserialization failed"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Ruvector WASM Runtime
|
|
|
|
/// Main entry point for Ruvector WASM on iOS
|
|
/// Uses WasmKit for native WASM execution
|
|
public final class RuvectorWasm {
|
|
|
|
/// Shared instance (singleton pattern for resource efficiency)
|
|
public static let shared = RuvectorWasm()
|
|
|
|
// WASM runtime state
|
|
private var isLoaded = false
|
|
private var wasmBytes: Data?
|
|
private var memoryPtr: UnsafeMutableRawPointer?
|
|
private var memorySize: Int = 0
|
|
|
|
// Learning state handles
|
|
private var healthLearnerHandle: Int32 = -1
|
|
private var locationLearnerHandle: Int32 = -1
|
|
private var commLearnerHandle: Int32 = -1
|
|
private var calendarLearnerHandle: Int32 = -1
|
|
private var appUsageLearnerHandle: Int32 = -1
|
|
private var iosLearnerHandle: Int32 = -1
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Initialization
|
|
|
|
/// Load WASM module from bundle
|
|
/// - Parameter bundlePath: Path to .wasm file in app bundle
|
|
public func load(from bundlePath: String) throws {
|
|
guard let data = FileManager.default.contents(atPath: bundlePath) else {
|
|
throw RuvectorError.initializationFailed("WASM file not found at \(bundlePath)")
|
|
}
|
|
try load(wasmData: data)
|
|
}
|
|
|
|
/// Load WASM module from data
|
|
/// - Parameter wasmData: Raw WASM bytes
|
|
public func load(wasmData: Data) throws {
|
|
self.wasmBytes = wasmData
|
|
|
|
// In production: Initialize WasmKit runtime here
|
|
// For now, mark as loaded for API design
|
|
// TODO: Integrate WasmKit when added as dependency
|
|
//
|
|
// Example WasmKit integration:
|
|
// let module = try WasmKit.Module(bytes: [UInt8](wasmData))
|
|
// let instance = try module.instantiate()
|
|
// self.wasmInstance = instance
|
|
|
|
isLoaded = true
|
|
}
|
|
|
|
/// Check if WASM is loaded
|
|
public var isReady: Bool { isLoaded }
|
|
|
|
// MARK: - Memory Management
|
|
|
|
/// Allocate memory in WASM linear memory
|
|
private func allocate(size: Int) throws -> Int {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call wasm_alloc export
|
|
return 0
|
|
}
|
|
|
|
/// Free memory in WASM linear memory
|
|
private func free(ptr: Int, size: Int) throws {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call wasm_free export
|
|
}
|
|
|
|
// MARK: - SIMD Operations
|
|
|
|
/// Compute dot product of two vectors
|
|
public func dotProduct(_ a: [Float], _ b: [Float]) throws -> Float {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
guard a.count == b.count else {
|
|
throw RuvectorError.invalidInput("Vectors must have same length")
|
|
}
|
|
|
|
// Pure Swift fallback (SIMD when available)
|
|
var result: Float = 0
|
|
for i in 0..<a.count {
|
|
result += a[i] * b[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// Compute L2 distance
|
|
public func l2Distance(_ a: [Float], _ b: [Float]) throws -> Float {
|
|
guard a.count == b.count else {
|
|
throw RuvectorError.invalidInput("Vectors must have same length")
|
|
}
|
|
|
|
var sum: Float = 0
|
|
for i in 0..<a.count {
|
|
let diff = a[i] - b[i]
|
|
sum += diff * diff
|
|
}
|
|
return sqrt(sum)
|
|
}
|
|
|
|
/// Compute cosine similarity
|
|
public func cosineSimilarity(_ a: [Float], _ b: [Float]) throws -> Float {
|
|
guard a.count == b.count else {
|
|
throw RuvectorError.invalidInput("Vectors must have same length")
|
|
}
|
|
|
|
var dot: Float = 0
|
|
var normA: Float = 0
|
|
var normB: Float = 0
|
|
|
|
for i in 0..<a.count {
|
|
dot += a[i] * b[i]
|
|
normA += a[i] * a[i]
|
|
normB += b[i] * b[i]
|
|
}
|
|
|
|
let denom = sqrt(normA) * sqrt(normB)
|
|
return denom > 0 ? dot / denom : 0
|
|
}
|
|
|
|
// MARK: - iOS Learner (Unified)
|
|
|
|
/// Initialize unified iOS learner
|
|
public func initIOSLearner() throws {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_learner_init export
|
|
iosLearnerHandle = 0
|
|
}
|
|
|
|
/// Update health metrics
|
|
public func updateHealth(_ state: HealthState) throws {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_update_health export
|
|
}
|
|
|
|
/// Update location
|
|
public func updateLocation(_ state: LocationState) throws {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_update_location export
|
|
}
|
|
|
|
/// Update communication patterns
|
|
public func updateCommunication(_ state: CommState) throws {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_update_communication export
|
|
}
|
|
|
|
/// Update calendar
|
|
public func updateCalendar(_ event: CalendarEvent) throws {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_update_calendar export
|
|
}
|
|
|
|
/// Update app usage
|
|
public func updateAppUsage(_ session: AppUsageSession) throws {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_update_app_usage export
|
|
}
|
|
|
|
/// Get context-aware recommendations
|
|
public func getRecommendations(_ context: IOSContext) throws -> ContextRecommendations {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
|
|
// TODO: Call ios_get_recommendations export
|
|
// For now, return sensible defaults
|
|
return ContextRecommendations(
|
|
suggestedAppCategory: .productivity,
|
|
focusScore: 0.7,
|
|
activitySuggestions: [
|
|
ActivitySuggestion(category: "Focus", confidence: 0.8, reason: "Good time for deep work")
|
|
],
|
|
optimalNotificationTime: context.hour >= 9 && context.hour <= 18
|
|
)
|
|
}
|
|
|
|
/// Train one iteration (call periodically)
|
|
public func trainIteration() throws {
|
|
guard iosLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call ios_train export
|
|
}
|
|
|
|
// MARK: - Calendar Learning
|
|
|
|
/// Initialize calendar learner
|
|
public func initCalendarLearner() throws {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call calendar_init export
|
|
calendarLearnerHandle = 0
|
|
}
|
|
|
|
/// Learn from calendar event
|
|
public func learnCalendarEvent(_ event: CalendarEvent) throws {
|
|
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call calendar_learn_event export
|
|
}
|
|
|
|
/// Get busy probability for time slot
|
|
public func calendarBusyProbability(hour: UInt8, dayOfWeek: UInt8) throws -> Float {
|
|
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call calendar_is_busy export
|
|
return 0.5
|
|
}
|
|
|
|
/// Suggest focus times
|
|
public func suggestFocusTimes(durationHours: UInt8) throws -> [FocusTimeSuggestion] {
|
|
guard calendarLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call through WASM
|
|
return [
|
|
FocusTimeSuggestion(day: 1, startHour: 9, score: 0.9),
|
|
FocusTimeSuggestion(day: 2, startHour: 14, score: 0.85)
|
|
]
|
|
}
|
|
|
|
// MARK: - App Usage Learning
|
|
|
|
/// Initialize app usage learner
|
|
public func initAppUsageLearner() throws {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call app_usage_init export
|
|
appUsageLearnerHandle = 0
|
|
}
|
|
|
|
/// Learn from app session
|
|
public func learnAppSession(_ session: AppUsageSession) throws {
|
|
guard appUsageLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call app_usage_learn export
|
|
}
|
|
|
|
/// Get screen time (hours)
|
|
public func screenTime() throws -> Float {
|
|
guard appUsageLearnerHandle >= 0 else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call app_usage_screen_time export
|
|
return 2.5
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
/// Serialize all learning state
|
|
public func serialize() throws -> Data {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call serialize exports for each learner
|
|
return Data()
|
|
}
|
|
|
|
/// Deserialize learning state
|
|
public func deserialize(_ data: Data) throws {
|
|
guard isLoaded else { throw RuvectorError.wasmNotLoaded }
|
|
// TODO: Call deserialize exports
|
|
}
|
|
|
|
/// Save state to file
|
|
public func save(to url: URL) throws {
|
|
let data = try serialize()
|
|
try data.write(to: url)
|
|
}
|
|
|
|
/// Load state from file
|
|
public func restore(from url: URL) throws {
|
|
let data = try Data(contentsOf: url)
|
|
try deserialize(data)
|
|
}
|
|
}
|
|
|
|
// MARK: - SwiftUI Integration
|
|
|
|
#if canImport(SwiftUI)
|
|
import SwiftUI
|
|
|
|
/// Observable wrapper for SwiftUI
|
|
@available(iOS 15.0, macOS 12.0, *)
|
|
@MainActor
|
|
public final class RuvectorViewModel: ObservableObject {
|
|
@Published public private(set) var isReady = false
|
|
@Published public private(set) var recommendations: ContextRecommendations?
|
|
@Published public private(set) var screenTimeHours: Float = 0
|
|
@Published public private(set) var focusScore: Float = 0
|
|
|
|
private let runtime = RuvectorWasm.shared
|
|
|
|
public init() {}
|
|
|
|
/// Load WASM module
|
|
public func load(from bundlePath: String) async throws {
|
|
try runtime.load(from: bundlePath)
|
|
try runtime.initIOSLearner()
|
|
try runtime.initCalendarLearner()
|
|
try runtime.initAppUsageLearner()
|
|
isReady = true
|
|
}
|
|
|
|
/// Update recommendations for current context
|
|
public func updateRecommendations(context: IOSContext) async throws {
|
|
recommendations = try runtime.getRecommendations(context)
|
|
focusScore = recommendations?.focusScore ?? 0
|
|
}
|
|
|
|
/// Update screen time
|
|
public func updateScreenTime() async throws {
|
|
screenTimeHours = try runtime.screenTime()
|
|
}
|
|
|
|
/// Record app usage
|
|
public func recordAppUsage(_ session: AppUsageSession) async throws {
|
|
try runtime.learnAppSession(session)
|
|
try await updateScreenTime()
|
|
}
|
|
|
|
/// Record calendar event
|
|
public func recordCalendarEvent(_ event: CalendarEvent) async throws {
|
|
try runtime.learnCalendarEvent(event)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Combine Integration
|
|
|
|
#if canImport(Combine)
|
|
import Combine
|
|
|
|
@available(iOS 13.0, macOS 10.15, *)
|
|
extension RuvectorWasm {
|
|
/// Publisher for periodic training
|
|
public func trainingPublisher(interval: TimeInterval = 60) -> AnyPublisher<Void, Never> {
|
|
Timer.publish(every: interval, on: .main, in: .common)
|
|
.autoconnect()
|
|
.map { [weak self] _ in
|
|
try? self?.trainIteration()
|
|
}
|
|
.eraseToAnyPublisher()
|
|
}
|
|
}
|
|
#endif
|