1use serde::Deserialize;
4use tensor_data::F32Tensor;
5use video_analysis_core::runtime::{
6 OperationId, PackageSurface, RuntimeCapabilities, SurfaceOperation, SurfaceRequest,
7 SurfaceResponse,
8};
9
10use crate::{F32Matrix, Kernel1d, MatrixShape};
11
12const MAX_VALUES: usize = 100_000;
13
14pub fn package_surface() -> PackageSurface {
16 PackageSurface {
17 library: env!("CARGO_PKG_NAME").to_string(),
18 version: env!("CARGO_PKG_VERSION").to_string(),
19 capabilities: RuntimeCapabilities::pure_rust(),
20 operations: vec![
21 operation(
22 "describe",
23 "Describe package",
24 "Dense matrix and kernel contracts bridging tensor-data and vector-analysis-core.",
25 serde_json::json!({"includeOperations": true}),
26 ),
27 operation(
28 "linear.matmul",
29 "Matrix multiply",
30 "Multiplies two finite f32 row-major matrices.",
31 serde_json::json!({
32 "left": {"rows": 2, "cols": 2, "values": [1.0, 2.0, 3.0, 4.0]},
33 "right": {"rows": 2, "cols": 1, "values": [5.0, 6.0]}
34 }),
35 ),
36 operation(
37 "linear.transpose",
38 "Matrix transpose",
39 "Returns a row-major owned transpose of a finite f32 matrix.",
40 serde_json::json!({
41 "matrix": {"rows": 2, "cols": 3, "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}
42 }),
43 ),
44 operation(
45 "linear.solve",
46 "Linear solve",
47 "Solves a square finite f32 matrix against a vector or matrix right-hand side.",
48 serde_json::json!({
49 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]},
50 "rhs": [1.0, 2.0]
51 }),
52 ),
53 operation(
54 "linear.decompose",
55 "LU decomposition",
56 "Decomposes a square finite f32 matrix with partial-pivoted LU.",
57 serde_json::json!({
58 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]}
59 }),
60 ),
61 operation(
62 "linear.inverse",
63 "Matrix inverse",
64 "Returns the inverse of a finite square f32 matrix.",
65 serde_json::json!({
66 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]}
67 }),
68 ),
69 operation(
70 "linear.kernel1d",
71 "1D kernel",
72 "Validates and optionally normalizes a finite 1D f32 kernel.",
73 serde_json::json!({"values": [0.25, 0.5, 0.25], "normalize": true}),
74 ),
75 operation(
76 "linear.tensorBridge",
77 "Tensor matrix bridge",
78 "Projects rank-2 tensor payloads to matrix shape or matrix-shaped payloads to tensor shape.",
79 serde_json::json!({"shape": [2, 2], "values": [1.0, 2.0, 3.0, 4.0], "direction": "tensorToMatrix"}),
80 ),
81 ],
82 }
83}
84
85fn operation(
86 id: &str,
87 name: &str,
88 description: &str,
89 example_request: serde_json::Value,
90) -> SurfaceOperation {
91 SurfaceOperation {
92 id: OperationId::new(id),
93 name: name.to_string(),
94 description: Some(description.to_string()),
95 input_schema: serde_json::json!({"type": "object", "additionalProperties": true}),
96 output_schema: serde_json::json!({"type": "object"}),
97 example_request,
98 wasm_supported: true,
99 server_supported: true,
100 }
101}
102
103pub fn run_surface_operation(request: SurfaceRequest) -> Result<SurfaceResponse, String> {
105 let operation = request.operation.clone();
106 let value = match request.operation.as_str() {
107 "describe" => describe_value(request.input),
108 "linear.matmul" => matmul_value(parse_input(request.input)?)?,
109 "linear.transpose" => transpose_value(parse_input(request.input)?)?,
110 "linear.solve" => solve_value(parse_input(request.input)?)?,
111 "linear.decompose" => decompose_value(parse_input(request.input)?)?,
112 "linear.inverse" => inverse_value(parse_input(request.input)?)?,
113 "linear.kernel1d" => kernel1d_value(parse_input(request.input)?)?,
114 "linear.tensorBridge" => tensor_bridge_value(parse_input(request.input)?)?,
115 operation => {
116 return Err(format!(
117 "unsupported operation `{operation}` for {}",
118 env!("CARGO_PKG_NAME")
119 ));
120 }
121 };
122 Ok(response(operation, value))
123}
124
125fn describe_value(input: serde_json::Value) -> serde_json::Value {
126 let surface = package_surface();
127 serde_json::json!({
128 "library": surface.library,
129 "version": surface.version,
130 "operationCount": surface.operations.len(),
131 "operations": surface
132 .operations
133 .iter()
134 .map(|operation| operation.id.as_str())
135 .collect::<Vec<_>>(),
136 "input": input
137 })
138}
139
140fn response(operation: OperationId, value: serde_json::Value) -> SurfaceResponse {
141 SurfaceResponse {
142 operation,
143 value,
144 diagnostics: Vec::new(),
145 artifacts: Vec::new(),
146 }
147}
148
149#[derive(Debug, Deserialize)]
150#[serde(rename_all = "camelCase")]
151struct MatmulRequest {
152 left: MatrixRequest,
153 right: MatrixRequest,
154}
155
156#[derive(Debug, Deserialize)]
157#[serde(rename_all = "camelCase")]
158struct UnaryMatrixRequest {
159 matrix: MatrixRequest,
160}
161
162#[derive(Debug, Deserialize)]
163#[serde(rename_all = "camelCase")]
164struct SolveRequest {
165 matrix: MatrixRequest,
166 #[serde(default)]
167 rhs: Option<Vec<f32>>,
168 #[serde(default)]
169 rhs_matrix: Option<MatrixRequest>,
170}
171
172#[derive(Debug, Deserialize)]
173#[serde(rename_all = "camelCase")]
174struct MatrixRequest {
175 rows: usize,
176 cols: usize,
177 values: Vec<f32>,
178}
179
180#[derive(Debug, Deserialize)]
181#[serde(rename_all = "camelCase")]
182struct KernelRequest {
183 values: Vec<f32>,
184 #[serde(default)]
185 normalize: bool,
186}
187
188#[derive(Debug, Deserialize)]
189#[serde(rename_all = "camelCase")]
190struct TensorBridgeRequest {
191 shape: Vec<usize>,
192 values: Vec<f32>,
193 direction: String,
194}
195
196fn matmul_value(request: MatmulRequest) -> Result<serde_json::Value, String> {
197 let left = matrix_from_request(request.left)?;
198 let right = matrix_from_request(request.right)?;
199 let product = left
200 .matmul(&right.as_view())
201 .map_err(|error| error.to_string())?;
202 matrix_json(product)
203}
204
205fn transpose_value(request: UnaryMatrixRequest) -> Result<serde_json::Value, String> {
206 let matrix = matrix_from_request(request.matrix)?;
207 let transpose = matrix
208 .as_view()
209 .transpose_owned()
210 .map_err(|error| error.to_string())?;
211 matrix_json(transpose)
212}
213
214fn solve_value(request: SolveRequest) -> Result<serde_json::Value, String> {
215 if request.rhs.is_some() && request.rhs_matrix.is_some() {
216 return Err("linear.solve accepts either rhs or rhsMatrix, not both".to_string());
217 }
218 if let Some(rhs) = request.rhs.as_ref() {
219 validate_value_count(rhs.len())?;
220 }
221 if let Some(rhs_matrix) = request.rhs_matrix.as_ref() {
222 validate_value_count(rhs_matrix.values.len())?;
223 }
224 let matrix = matrix_from_request(request.matrix)?;
225 let decomposition = matrix
226 .as_view()
227 .lu_decompose()
228 .map_err(|error| error.to_string())?;
229 let determinant = decomposition
230 .determinant()
231 .map_err(|error| error.to_string())?;
232
233 match (request.rhs, request.rhs_matrix) {
234 (Some(_), Some(_)) => unreachable!("checked above"),
235 (Some(rhs), None) => {
236 let solution = decomposition
237 .solve_vector(&rhs)
238 .map_err(|error| error.to_string())?;
239 Ok(serde_json::json!({
240 "solution": solution,
241 "determinant": determinant
242 }))
243 }
244 (None, Some(rhs_matrix)) => {
245 let rhs = matrix_from_request(rhs_matrix)?;
246 let solution = decomposition
247 .solve_matrix(&rhs.as_view())
248 .map_err(|error| error.to_string())?;
249 let shape = solution.shape();
250 Ok(serde_json::json!({
251 "solutionMatrix": {
252 "rows": shape.rows,
253 "cols": shape.cols,
254 "values": solution.values()
255 },
256 "determinant": determinant
257 }))
258 }
259 (None, None) => Err("linear.solve requires rhs or rhsMatrix".to_string()),
260 }
261}
262
263fn decompose_value(request: UnaryMatrixRequest) -> Result<serde_json::Value, String> {
264 let matrix = matrix_from_request(request.matrix)?;
265 let decomposition = matrix
266 .as_view()
267 .lu_decompose()
268 .map_err(|error| error.to_string())?;
269 let determinant = decomposition
270 .determinant()
271 .map_err(|error| error.to_string())?;
272 let lower = decomposition
273 .lower_matrix()
274 .map_err(|error| error.to_string())?;
275 let upper = decomposition
276 .upper_matrix()
277 .map_err(|error| error.to_string())?;
278 let shape = decomposition.shape();
279 Ok(serde_json::json!({
280 "method": "lu",
281 "rows": shape.rows,
282 "cols": shape.cols,
283 "pivots": decomposition.pivots(),
284 "swapCount": decomposition.swap_count(),
285 "determinant": determinant,
286 "lower": {
287 "rows": lower.shape().rows,
288 "cols": lower.shape().cols,
289 "values": lower.values()
290 },
291 "upper": {
292 "rows": upper.shape().rows,
293 "cols": upper.shape().cols,
294 "values": upper.values()
295 }
296 }))
297}
298
299fn inverse_value(request: UnaryMatrixRequest) -> Result<serde_json::Value, String> {
300 let matrix = matrix_from_request(request.matrix)?;
301 let inverse = matrix.inverse().map_err(|error| error.to_string())?;
302 matrix_json(inverse)
303}
304
305fn kernel1d_value(request: KernelRequest) -> Result<serde_json::Value, String> {
306 validate_value_count(request.values.len())?;
307 let kernel = Kernel1d::new(request.values).map_err(|error| error.to_string())?;
308 let sum = kernel.values().iter().sum::<f32>();
309 let mut value = serde_json::json!({
310 "len": kernel.values().len(),
311 "sum": sum,
312 "center": kernel.values().len() / 2,
313 "values": kernel.values()
314 });
315 if request.normalize {
316 if sum.abs() <= f32::EPSILON {
317 return Err("1D kernel sum must be non-zero to normalize".to_string());
318 }
319 value["normalizedValues"] = serde_json::json!(kernel
320 .values()
321 .iter()
322 .map(|value| value / sum)
323 .collect::<Vec<_>>());
324 }
325 Ok(value)
326}
327
328fn tensor_bridge_value(request: TensorBridgeRequest) -> Result<serde_json::Value, String> {
329 validate_value_count(request.values.len())?;
330 match request.direction.as_str() {
331 "tensorToMatrix" => {
332 let tensor = F32Tensor::from_dims(request.shape, request.values)
333 .map_err(|error| error.to_string())?;
334 let matrix = F32Matrix::try_from(&tensor).map_err(|error| error.to_string())?;
335 let shape = matrix.shape();
336 Ok(serde_json::json!({
337 "direction": "tensorToMatrix",
338 "shape": [shape.rows, shape.cols],
339 "rows": shape.rows,
340 "cols": shape.cols,
341 "values": matrix.values()
342 }))
343 }
344 "matrixToTensor" => {
345 if request.shape.len() != 2 {
346 return Err("matrixToTensor requires exactly two shape dimensions".to_string());
347 }
348 let matrix = F32Matrix::new(
349 MatrixShape::new(request.shape[0], request.shape[1])
350 .map_err(|error| error.to_string())?,
351 request.values,
352 )
353 .map_err(|error| error.to_string())?;
354 let tensor = F32Tensor::try_from(&matrix).map_err(|error| error.to_string())?;
355 Ok(serde_json::json!({
356 "direction": "matrixToTensor",
357 "shape": tensor.shape().dimensions(),
358 "values": tensor.values()
359 }))
360 }
361 direction => Err(format!("unsupported tensor bridge direction `{direction}`")),
362 }
363}
364
365fn matrix_from_request(request: MatrixRequest) -> Result<F32Matrix, String> {
366 validate_value_count(request.values.len())?;
367 F32Matrix::new(
368 MatrixShape::new(request.rows, request.cols).map_err(|error| error.to_string())?,
369 request.values,
370 )
371 .map_err(|error| error.to_string())
372}
373
374fn matrix_json(matrix: F32Matrix) -> Result<serde_json::Value, String> {
375 let shape = matrix.shape();
376 Ok(serde_json::json!({
377 "rows": shape.rows,
378 "cols": shape.cols,
379 "values": matrix.values()
380 }))
381}
382
383fn validate_value_count(count: usize) -> Result<(), String> {
384 if count > MAX_VALUES {
385 return Err(format!("values must not exceed {MAX_VALUES}"));
386 }
387 Ok(())
388}
389
390fn parse_input<T: for<'de> Deserialize<'de>>(input: serde_json::Value) -> Result<T, String> {
391 serde_json::from_value(input).map_err(|error| format!("invalid request: {error}"))
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 fn assert_close(left: f32, right: f32) {
399 assert!((left - right).abs() < 1.0e-4, "expected {left} ≈ {right}");
400 }
401
402 fn f32_array(value: &serde_json::Value) -> Vec<f32> {
403 serde_json::from_value(value.clone()).expect("f32 array")
404 }
405
406 #[test]
407 fn matmul_returns_product() {
408 let response = run_surface_operation(SurfaceRequest {
409 operation: OperationId::new("linear.matmul"),
410 input: serde_json::json!({
411 "left": {"rows": 2, "cols": 2, "values": [1.0, 2.0, 3.0, 4.0]},
412 "right": {"rows": 2, "cols": 1, "values": [5.0, 6.0]}
413 }),
414 })
415 .expect("matmul operation");
416
417 assert_eq!(response.value["rows"], 2);
418 assert_eq!(response.value["cols"], 1);
419 assert_eq!(response.value["values"], serde_json::json!([17.0, 39.0]));
420 }
421
422 #[test]
423 fn kernel_normalizes_values() {
424 let response = run_surface_operation(SurfaceRequest {
425 operation: OperationId::new("linear.kernel1d"),
426 input: serde_json::json!({"values": [1.0, 1.0, 2.0], "normalize": true}),
427 })
428 .expect("kernel operation");
429
430 assert_eq!(response.value["len"], 3);
431 assert_eq!(response.value["center"], 1);
432 assert_eq!(
433 response.value["normalizedValues"],
434 serde_json::json!([0.25, 0.25, 0.5])
435 );
436 }
437
438 #[test]
439 fn tensor_bridge_reports_matrix_shape() {
440 let response = run_surface_operation(SurfaceRequest {
441 operation: OperationId::new("linear.tensorBridge"),
442 input: serde_json::json!({"shape": [2, 2], "values": [1.0, 2.0, 3.0, 4.0], "direction": "tensorToMatrix"}),
443 })
444 .expect("tensor bridge operation");
445
446 assert_eq!(response.value["rows"], 2);
447 assert_eq!(response.value["cols"], 2);
448 }
449
450 #[test]
451 fn transpose_returns_expected_shape_and_values() {
452 let response = run_surface_operation(SurfaceRequest {
453 operation: OperationId::new("linear.transpose"),
454 input: serde_json::json!({
455 "matrix": {"rows": 2, "cols": 3, "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}
456 }),
457 })
458 .expect("transpose operation");
459
460 assert_eq!(response.value["rows"], 3);
461 assert_eq!(response.value["cols"], 2);
462 assert_eq!(
463 response.value["values"],
464 serde_json::json!([1.0, 4.0, 2.0, 5.0, 3.0, 6.0])
465 );
466 }
467
468 #[test]
469 fn solve_with_vector_rhs_returns_solution() {
470 let response = run_surface_operation(SurfaceRequest {
471 operation: OperationId::new("linear.solve"),
472 input: serde_json::json!({
473 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]},
474 "rhs": [1.0, 2.0]
475 }),
476 })
477 .expect("solve operation");
478
479 let solution = f32_array(&response.value["solution"]);
480 assert_close(solution[0], 0.2);
481 assert_close(solution[1], 0.6);
482 assert_close(
483 serde_json::from_value(response.value["determinant"].clone()).unwrap(),
484 5.0,
485 );
486 }
487
488 #[test]
489 fn solve_with_matrix_rhs_returns_inverse_like_result() {
490 let response = run_surface_operation(SurfaceRequest {
491 operation: OperationId::new("linear.solve"),
492 input: serde_json::json!({
493 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]},
494 "rhsMatrix": {"rows": 2, "cols": 2, "values": [1.0, 0.0, 0.0, 1.0]}
495 }),
496 })
497 .expect("solve operation");
498
499 assert_eq!(response.value["solutionMatrix"]["rows"], 2);
500 assert_eq!(response.value["solutionMatrix"]["cols"], 2);
501 let values = f32_array(&response.value["solutionMatrix"]["values"]);
502 assert_close(values[0], 0.6);
503 assert_close(values[1], -0.2);
504 assert_close(values[2], -0.2);
505 assert_close(values[3], 0.4);
506 }
507
508 #[test]
509 fn decompose_returns_lower_and_upper_objects() {
510 let response = run_surface_operation(SurfaceRequest {
511 operation: OperationId::new("linear.decompose"),
512 input: serde_json::json!({
513 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]}
514 }),
515 })
516 .expect("decompose operation");
517
518 assert_eq!(response.value["method"], "lu");
519 assert_eq!(response.value["lower"]["rows"], 2);
520 assert_eq!(response.value["upper"]["cols"], 2);
521 assert!(response.value["lower"]["values"].is_array());
522 assert!(response.value["upper"]["values"].is_array());
523 }
524
525 #[test]
526 fn inverse_returns_expected_values() {
527 let response = run_surface_operation(SurfaceRequest {
528 operation: OperationId::new("linear.inverse"),
529 input: serde_json::json!({
530 "matrix": {"rows": 2, "cols": 2, "values": [2.0, 1.0, 1.0, 3.0]}
531 }),
532 })
533 .expect("inverse operation");
534
535 let values = f32_array(&response.value["values"]);
536 assert_close(values[0], 0.6);
537 assert_close(values[1], -0.2);
538 assert_close(values[2], -0.2);
539 assert_close(values[3], 0.4);
540 }
541}