How to Contribute¶
Oryon accepts contributions of features and targets. The core traits, pipeline, and PyO3 layer follow a fixed pattern - new implementations slot in without touching the architecture.
Every contribution touches both layers: the Rust core and the Python binding. The steps below are mandatory and in order.
Adding a Feature¶
1. Write the Rust struct¶
Create crates/oryon/src/features/<your_feature>.rs.
Implement the Feature trait. Use Sma as your reference implementation.
use crate::error::OryonError;
use crate::traits::{Feature, Output};
use smallvec::smallvec;
pub struct YourFeature {
inputs: Vec<String>,
window: usize,
outputs: Vec<String>,
// internal state fields last
}
impl YourFeature {
pub fn new(
inputs: Vec<String>,
window: usize,
outputs: Vec<String>,
) -> Result<Self, OryonError> {
if inputs.is_empty() {
return Err(OryonError::InvalidInput { msg: "inputs must not be empty".into() });
}
if outputs.is_empty() {
return Err(OryonError::InvalidInput { msg: "outputs must not be empty".into() });
}
if window == 0 {
return Err(OryonError::InvalidInput { msg: "window must be non-zero".into() });
}
Ok(YourFeature { inputs, window, outputs })
}
}
impl Feature for YourFeature {
fn input_names(&self) -> Vec<String> { self.inputs.clone() }
fn output_names(&self) -> Vec<String> { self.outputs.clone() }
fn warm_up_period(&self) -> usize { self.window - 1 }
fn fresh(&self) -> Box<dyn Feature> {
Box::new(YourFeature::new(self.inputs.clone(), self.window, self.outputs.clone())
.expect("fresh: config was already validated at construction"))
}
fn reset(&mut self) {
// clear internal state
}
fn update(&mut self, state: &[Option<f64>]) -> Output {
// compute and return
smallvec![None]
}
}
Rules:
-
state[i]maps toinput_names()[i]in order. -
Return
smallvec![None]during warm-up or onNoneinput propagation. -
fresh()must return a clean-state instance with the same config. -
No
.unwrap()onResult- use?or returnOryonError. Unwrapping anOptionis fine when the surrounding code logically guaranteesSome.
2. Register in mod.rs¶
In crates/oryon/src/features/mod.rs, add:
3. Write tests¶
Tests go in the same file, inside #[cfg(test)]. See Test Templates for the mandatory structure and order.
4. Write benchmarks¶
In benches/features.rs, add two benchmark groups:
c.bench_function("your_feature_update/w20", |b| { ... });
c.bench_function("your_feature_update/w200", |b| { ... });
Run with:
Target: update under 1µs at w200. Note it in the PR if exceeded.
5. Add the PyO3 wrapper¶
In crates/oryon-python/src/features.rs, add a wrapper following the Sma pattern exactly:
use oryon::features::YourFeature as RustYourFeature;
#[pyclass(module = "oryon")]
pub(crate) struct YourFeature {
pub(crate) inner: RustYourFeature,
}
#[pymethods]
impl YourFeature {
#[new]
pub fn new(inputs: Vec<String>, window: usize, outputs: Vec<String>) -> PyResult<Self> {
let inner = RustYourFeature::new(inputs, window, outputs)
.map_err(|e| PyValueError::new_err(e.to_string()))?;
Ok(YourFeature { inner })
}
fn update(&mut self, values: Vec<f64>) -> Vec<f64> {
to_python(&self.inner.update(&to_rust(&values)))
}
fn reset(&mut self) { self.inner.reset(); }
fn input_names(&self) -> Vec<String> { self.inner.input_names() }
fn output_names(&self) -> Vec<String> { self.inner.output_names() }
fn warm_up_period(&self) -> usize { self.inner.warm_up_period() }
fn __repr__(&self) -> String {
format!("YourFeature(inputs={:?}, window={}, outputs={:?})",
self.inner.input_names(), /* window */ 0, self.inner.output_names())
}
}
6. Register in lib.rs¶
In crates/oryon-python/src/lib.rs, add your type in three places:
// 1. use statement at the top
use features::YourFeature;
// 2. branch in extract_feature()
if let Ok(f) = obj.extract::<PyRef<YourFeature>>() {
return Ok(f.inner.fresh());
}
// 3. module registration
m.add_class::<YourFeature>()?;
7. Re-export in Python¶
Two files to update:
In python/oryon/features.py, add YourFeature to the import from ._oryon and to __all__.
In python/oryon/__init__.py, add YourFeature to the import from .features and to __all__.
8. Write documentation¶
Add an entry to the correct API Reference page. See Doc Templates.
Adding a Target¶
The process mirrors adding a feature, with three differences:
- Files go in
crates/oryon/src/targets/andcrates/oryon-python/src/targets.rs. - Implement the
Targettrait instead ofFeature(noreset(), nofresh(), statelessrun_research()). extract_target()inlib.rsreconstructs the target from stored params (seeFutureReturnas reference).
See Architecture for the full Target trait interface.
Before opening a PR¶
Run the full check suite locally:
make lint # cargo fmt + clippy + cargo doc
make test # cargo test + pytest (requires maturin develop)
make bench-rust
CI must be green before merge.
Checklist:
- [ ]
make lintpasses (no fmt diff, no clippy warnings, docs compile) - [ ]
make testpasses (Rust + Python) - [ ] Benchmarks added with the correct naming convention (the benchmark page is updated by the maintainer before each release)
- [ ] PyO3 wrapper added and registered in all three places in
lib.rs - [ ] Python re-export updated in
python/oryon/features.py(ortargets.py) andpython/oryon/__init__.py - [ ] Documentation entry added using the template
- [ ] No
.unwrap()onResultin library code - use?or returnOryonError::InvalidInput - [ ] Constructor validates all parameters and returns
OryonError::InvalidInput - [ ]
output_names()pattern follows{input}_{name}_{param}