Rust Docs
이 문서는 Hospital-at-Home 예제에서 Rust가 맡는 signal preprocessing 책임을 설명합니다. Rust는 raw 또는 lightly processed signal을 Python risk workflow가 소비할 feature contract로 바꾸고, clinical risk 판단이나 HTTP runtime은 담당하지 않습니다.
Rust 문서는 rustdoc을 기준으로 관리합니다.
cargo doc --workspace --open
crate public API는 /// doc comment를 source of truth로 둡니다. package consumer 문서는 crate README에 두고, app 운영 문서는 app README와 CD repo overlay에 둡니다.
1. Hospital-at-Home 구현 기준
이 시나리오에서 Rust는 Rust Python package signal preprocessing을 담당합니다. 1단계 dev-guide에서는 pure Rust core가 SignalWindow -> PreprocessedSignal 계산을 구현하고, Python은 PyO3/maturin binding package를 통해 이 기능을 import합니다. 고주파 waveform과 20 bed 이상 동시 처리 방향에서는 public contract를 SignalFrameSet -> SignalProcessingRunResult로 확장합니다. Rust HTTP service는 예제 범위에서 제외합니다.
1-1. 책임
Rust의 책임은 raw 또는 lightly processed signal window를 분석 가능한 feature contract로 바꾸는 것입니다.
Rust가 담당합니다.
SignalFrameSet단위 grouped signal processing- time-series window validation
- missingness와 artifact detection
- packet quality flag와 sample sequence continuity 반영
- signal quality 계산
- rolling baseline과 variability 계산
- SpO2 drop count, heart rate trend, motion burden, artifact score 같은 feature extraction
- preprocessing result의 typed contract 제공
- invalid state를 type과 explicit error로 줄이기
Rust가 담당하지 않습니다.
- clinical risk scoring
- patient clinical context repository
- model threshold policy
- clinician review queue 생성
- MQTT broker connection
- frontend view model
- HTTP service
1-2. Python 통합 방식
Python에서 Rust 기능을 사용하는 기본 방식은 PyO3 + maturin입니다. Pydantic v2의 pydantic-core처럼 Python package가 Rust-backed Python package을 포함하는 패턴을 따릅니다.
최종 구조는 아래와 같습니다.
packages/
patient-signal-rust-core/ pure Rust preprocessing crate
patient-signal-python-rust/ PyO3/maturin binding package
patient-risk-python-core/ Python risk orchestration
현재 구현은 patient-signal-rust-core에 pure preprocessing function을 두고, patient-signal-python-rust가 PyO3/maturin binding만 담당합니다. Python API app에는 같은 contract를 따르는 preprocessing port와 RustSignalPreprocessor outbound adapter가 있습니다. Python core는 이미 계산된 PreprocessedSignal을 받아 clinical result를 만드는 쪽에 머뭅니다.
patient-signal-python-rust는 root Cargo workspace와 uv workspace에는 포함하지 않습니다. 기본 Python/Rust package job이 Rust Python package build에 묶여 느려지거나 깨지지 않게 하기 위해서입니다. 대신 make package/test/python-rust와 source-repo-python workflow의 Rust Python package job이 명시적으로 PyO3 package를 build/test합니다.
이 package는 Python wheel이므로 배포 version은 PEP 440을 따릅니다. CI는 cargo set-version --manifest-path packages/patient-signal-python-rust/Cargo.toml <semver>로 Rust crate metadata를 맞추고, uv version --project packages/patient-signal-python-rust <pep440> --frozen으로 Python package metadata를 맞춘 뒤 maturin wheel을 빌드합니다. 이렇게 하면 Rust/Cargo 생태계와 Python/PyPI 생태계의 version 규칙을 억지로 하나로 합치지 않고, 같은 source revision에서 계산된 대응 version을 각각 적용할 수 있습니다.
1단계에서는 Rust preprocessing을 별도 network service로 띄우지 않습니다. Rust workflow도 core crate만 검증하며, HTTP service app/image는 1차 demo 산출물에서 제외합니다.
Python risk API
-> import patient_signal_python_rust
-> Rust preprocessing function
-> Python API preprocessing adapter
-> Python core clinical result builder
이 구조를 기본값으로 두는 이유는 아래와 같습니다.
- Python API에서 network hop 없이 preprocessing을 호출할 수 있습니다.
- Rust core는 Cargo test로 독립 검증합니다.
- Python binding은 wheel packaging 책임만 가집니다.
- 나중에 별도 Rust service가 필요해져도 pure Rust crate를 재사용할 수 있습니다. 단, 별도 service 구현은 1단계 예제 범위가 아닙니다.
1-3. Data boundary
초기에는 Python binding이 JSON-compatible dict/list 또는 Pydantic model에서 변환된 primitive 구조를 받습니다. 대용량 signal 처리가 필요해지면 memoryview, bytes, NumPy buffer, Arrow 같은 compact boundary를 검토합니다. 핵심은 Python이 waveform samples 전체를 dataclass object graph로 만들지 않고, metadata/envelope과 payload reference 또는 contiguous buffer를 Rust로 넘기는 것입니다.
Rust output은 Python risk model이 바로 소비할 수 있는 feature contract입니다.
SignalFrameSet
-> SignalProcessingRunResult
items[]
-> PreprocessedSignal
missing_ratio
artifact_score
signal_quality
heart_rate_trend
spo2_drop_count
baseline_deviation
현재 crate 구현은 SignalWindow -> PreprocessedSignal 단건 API를 먼저 제공합니다. 다음 확장에서는 단건 API를 크기 1인 SignalFrameSet wrapper로 유지하고, Rust/PyO3 public path는 set 단위 호출을 우선합니다. Python에서 20개 frame을 각각 호출하는 구조는 Python object 변환과 PyO3/GIL 경계 비용이 커지므로 피합니다.
성능 경계는 추정이 아니라 실행 가능한 smoke로 확인합니다. Rust core는 cargo run --example frame_set_boundary --release로 20 frame × 3,600 point processing smoke를 제공합니다. PyO3 경계는 scripts/benchmark-python-rust-frame-set.py가 같은 shape의 Python payload 생성 시간과 Rust package 호출 시간을 분리해 출력합니다. 이 값은 CI gate가 아니라 boundary review evidence입니다.
PreprocessedSignal에는 feature뿐 아니라 preprocessing_version도 포함합니다. PyO3 Rust Python package은 rust-signal-preprocessor-<crate-version>을 반환합니다. 이 값은 Python API의 auditContext.preprocessingVersion으로 그대로 전달되어, dashboard와 release evidence가 어떤 preprocessing engine으로 계산된 결과인지 추적할 수 있게 합니다.
missing_ratio는 contact loss나 SpO2 0처럼 실제 sample 안에서 무효화된 값뿐 아니라 packet-level dropped sample과 sequence gap도 반영합니다. artifact_score는 motion burden, low contact, sequence disorder, clipped/saturated/excessive motion flag를 함께 사용합니다. 이 값들은 demo feature extraction이며, 임상 성능 claim이나 의료기기 알고리즘 validation을 대체하지 않습니다.
Rust function은 오래 걸리는 연산에서 Python GIL을 오래 잡지 않도록 설계합니다.
1-4. Implementation 기준
Rust 예제는 wrapper만 두지 않고 실제 계산을 수행해야 합니다.
필수 구현 기준은 아래와 같습니다.
- fixture window parsing 또는 equivalent test input
SignalFrameSetcontract와 item별 partial failure 표현- missing ratio 계산
- SpO2 drop count 계산
- heart rate trend 계산
- motion burden 계산
- artifact score 계산
- dropped sample, sequence gap, clipped/saturated/excessive motion flag 반영
- typed validation error
- Python binding import smoke test
- Rust core unit test와 binding integration test 분리
현재 완료된 기반은 Rust core unit test, packet quality/sequence continuity 반영, Python Rust package contract test, PyO3/maturin package skeleton, Python binding import smoke test입니다. Rust Python package test는 shared signal fixture를 사용해 Rust core와 Python Rust package binding 결과가 같은 signal quality, feature contract, risk bucket을 유지하는지 확인합니다. RustSignalPreprocessor unit test는 Python API app adapter가 Rust Python package로 넘기는 payload shape를 고정하고, integration test는 missing ratio, artifact score, heart rate mean/trend, SpO2 drop, motion burden, baseline deviation까지 비교해 drift를 조기에 잡습니다. Local machine에 Rust toolchain이 있으면 make package/test/python-rust로 실제 import 경로와 Python API HTTP ingestion path가 Rust preprocessor를 사용하는지 확인합니다. 이 target은 doctor/rust를 먼저 실행하므로 rustup이나 cargo가 없는 환경에서는 maturin build 전에 실패합니다.
CI에서는 Rust core package workflow와 Python Rust package workflow를 함께 사용합니다. patient-signal-rust-core만 바뀌어도 Python workflow의 Rust Python package job이 실행되어 PyO3 binding이 Rust core 변경과 계속 맞는지 확인합니다.
release tag 또는 수동 publish에서는 Rust Python package wheel도 Python guide hosted repository에 publish합니다. Pure Python core package는 dist/python/*, PyO3/maturin Rust Python wheel은 dist/python-rust/*에 따로 생성해 troubleshooting 때 어떤 artifact가 실패했는지 바로 구분할 수 있게 둡니다.
Nexus PyPI credential이 없는 local machine에서는 아래처럼 uv option을 target에 넘깁니다.
make package/test/python-rust \
NATIVE_UV_RUN_ARGS="--default-index https://pypi.org/simple --index-strategy unsafe-best-match"
1-5. Test strategy
| Test | Scope |
|---|---|
| Rust unit test | pure feature calculation, validation error |
| Rust integration test | fixture window -> preprocessing result |
| Python binding test | Python input -> Rust extension -> Python object |
| Python API adapter test | signal packet -> Rust Python package -> PreprocessedSignal |
| Python core test | PreprocessedSignal -> clinical risk result |
1-6. Clean Architecture 기준
Rust에서 Clean Architecture는 Rust Python package performance code를 Python orchestration이나 HTTP runtime과 분리하는 기준입니다.
patient-signal-rust-core는 pure Rust domain/usecase crate입니다. PyO3, maturin, Python object conversion은 patient-signal-python-rust가 담당합니다. Python API app은 Rust crate를 직접 모르고 app-owned preprocessing adapter 뒤의 RustSignalPreprocessor를 선택합니다. Python core는 Rust, PyO3, HTTP runtime을 모릅니다. Rust package public language는 Batch가 아니라 SignalFrameSet, SignalProcessingRunResult, PreprocessedSignal을 사용합니다.
이 crate에는 사용하지 않는 ports/나 adapters/ scaffold를 미리 두지 않습니다. Rust core가 pure preprocessing library라면 domain.rs와 usecases.rs만으로도 충분합니다. 실제 file/network/device boundary가 생길 때만 port와 adapter를 추가합니다.
patient-risk-python-api
-> preprocessing outbound adapter
-> patient-signal-python-rust
-> patient-signal-rust-core
-> patient-risk-python-core clinical result builder
단단하게 고정할 것은 SignalFrameSet identity, item별 partial failure, PreprocessedSignal feature 의미와 validation error입니다. 느슨하게 둘 것은 Python binding payload shape, wheel packaging, 향후 NumPy/Arrow boundary입니다.
1-7. DDD 기준
Rust bounded context는 preprocessing입니다. Rust는 clinical risk를 만들지 않고, risk model이 소비할 feature와 signal quality evidence를 계산합니다.
| Term | Meaning |
|---|---|
SignalFrameSet |
preprocessing이 소비하는 grouped signal input |
SignalProcessingRunResult |
전체 run status와 item별 result/error |
SignalWindow |
현재 단건 implementation이 사용하는 time-series 입력 |
missing_ratio |
invalid sample, dropped sample, sequence gap을 반영한 결측성 |
artifact_score |
motion/contact/sequence/quality flag 기반 artifact evidence |
baseline_deviation |
window가 baseline에서 벗어난 정도 |
preprocessing_version |
Rust preprocessing engine provenance |
Rust feature가 곧 clinical diagnosis가 되면 context가 섞입니다. Rust output은 Python risk assessment의 입력 evidence로 유지합니다.
1-8. TDD 기준
Rust TDD는 pure calculation을 먼저 고정하고, binding은 그 다음에 검증합니다.
- Rust unit test로 feature formula와 validation error를 작성합니다.
- shared fixture integration test로 scenario별 feature direction을 확인합니다.
- PyO3 binding test로 Python payload와 Rust input 변환을 고정합니다.
- Python rust marker test로 Rust core/Rust Python package binding drift를 확인합니다.
- FastAPI Rust Python package smoke로 실제 ingestion path에서 provenance가 노출되는지 확인합니다.
이 순서를 따르면 Rust Python wheel packaging 문제가 pure preprocessing rule의 문제인지, binding/runtime 문제인지 분리할 수 있습니다.
2. Package Registry
Rust dependency download와 publish registry는 Cargo config를 통해 Nexus Cargo repositories를 사용합니다. .cargo/config.toml은 local 환경에 생성되는 파일이므로 commit하지 않습니다. Local 개발에서는 scripts/configure-nexus.sh가 생성하고, CI에서는 workflow가 직접 생성합니다.
NEXUS_CARGO_PROXY_REGISTRY=sparse+https://nexus.internal.tirosh.ai/repository/cargo/ \
NEXUS_CARGO_REGISTRY_INDEX=sparse+https://nexus.internal.tirosh.ai/repository/cargo-guide-hosted/ \
bash scripts/configure-nexus.sh
publish는 tirosh-nexus registry로 보냅니다.
cargo_token="Basic $(printf '%s:%s' "$NEXUS_USERNAME" "$NEXUS_PASSWORD" | base64 | tr -d '\n')"
CARGO_REGISTRIES_TIROSH_NEXUS_TOKEN="$cargo_token" \
cargo publish --locked -p patient-signal-rust-core --registry tirosh-nexus
3. Package Tests
cargo test --locked -p patient-signal-rust-core --lib
cargo test --locked -p patient-signal-rust-core --test preprocess_signal_window_integration
make package/test/python-rust
uv run --managed-python maturin develop -m packages/patient-signal-python-rust/Cargo.toml
uv run --managed-python --package patient-risk-python-core pytest packages/patient-risk-python-core/tests/integration -m rust
uv run --managed-python --package patient-risk-python-api pytest apps/patient-risk-python-api/tests -m rust
make package/build/python-rust
cargo package --locked -p patient-signal-rust-core --allow-dirty --no-verify