Skip to main content

math_linear/
surface.rs

1//! Library-owned runtime surface for `math-linear`.
2
3use 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
14/// Describes the linear algebra operations exposed by transport wrappers.
15pub 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
103/// Runs one library-owned operation.
104pub 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}