Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 189 additions & 162 deletions METADATA_STORAGE_V3.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/api/api_reference.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions python/ommx/ommx/_ommx_rust/__init__.pyi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions python/ommx/src/evaluated_named_function.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use pyo3::{prelude::*, Bound};
use std::collections::{HashMap, HashSet};

/// EvaluatedNamedFunction wrapper for Python
/// EvaluatedNamedFunction wrapper for Python.
///
/// Holds the Rust `EvaluatedNamedFunction` plus an owned snapshot of its
/// metadata. See `NamedFunction` for the snapshot-model rationale.
#[pyo3_stub_gen::derive::gen_stub_pyclass]
#[pyclass]
#[derive(Clone)]
pub struct EvaluatedNamedFunction(pub ommx::EvaluatedNamedFunction);
pub struct EvaluatedNamedFunction(
pub ommx::EvaluatedNamedFunction,
pub ommx::NamedFunctionMetadata,
);

#[pyo3_stub_gen::derive::gen_stub_pymethods]
#[pymethods]
Expand All @@ -22,22 +28,22 @@ impl EvaluatedNamedFunction {

#[getter]
pub fn name(&self) -> Option<String> {
self.0.name.clone()
self.1.name.clone()
}

#[getter]
pub fn subscripts(&self) -> Vec<i64> {
self.0.subscripts.clone()
self.1.subscripts.clone()
}

#[getter]
pub fn parameters(&self) -> HashMap<String, String> {
self.0.parameters.clone().into_iter().collect()
self.1.parameters.clone().into_iter().collect()
}

#[getter]
pub fn description(&self) -> Option<String> {
self.0.description.clone()
self.1.description.clone()
}

#[getter]
Expand Down
42 changes: 37 additions & 5 deletions python/ommx/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,15 @@ impl Instance {
builder = builder.sos1_constraints(rust_sos1_constraints);
}

let mut named_function_metadata_pairs: Vec<(
ommx::NamedFunctionID,
ommx::NamedFunctionMetadata,
)> = Vec::new();
if let Some(nfs) = named_functions {
let mut rust_named_functions = BTreeMap::new();
for nf in nfs {
let id = nf.0.id;
named_function_metadata_pairs.push((id, nf.1));
if rust_named_functions.insert(id, nf.0).is_some() {
anyhow::bail!("Duplicate named function ID: {}", id.into_inner());
}
Expand All @@ -201,6 +206,10 @@ impl Instance {
for (id, m) in indicator_metadata_pairs {
indicator_meta.insert(id, m);
}
let nf_meta = inner.named_function_metadata_mut();
for (id, m) in named_function_metadata_pairs {
nf_meta.insert(id, m);
}

Ok(Self {
inner,
Expand Down Expand Up @@ -458,10 +467,13 @@ impl Instance {
/// List of all named functions in the instance sorted by their IDs.
#[getter]
pub fn named_functions(&self) -> Vec<NamedFunction> {
let metadata = self.inner.named_function_metadata();
self.inner
.named_functions()
.values()
.map(|named_function| NamedFunction(named_function.clone()))
.iter()
.map(|(id, named_function)| {
NamedFunction(named_function.clone(), metadata.collect_for(*id))
})
.collect()
}

Expand Down Expand Up @@ -1825,7 +1837,21 @@ impl Instance {
include: Option<Vec<String>>,
) -> PyResult<Bound<'py, PyDataFrame>> {
let flags = crate::pandas::IncludeFlags::from_optional(include)?;
entries_to_dataframe(py, self.inner.named_functions().values(), "id", flags)
let nf_meta_store = self.inner.named_function_metadata().clone();
let nf_meta_view: Vec<(ommx::NamedFunctionMetadata, &ommx::NamedFunction)> = self
.inner
.named_functions()
.iter()
.map(|(id, nf)| (nf_meta_store.collect_for(*id), nf))
.collect();
entries_to_dataframe(
py,
nf_meta_view
.iter()
.map(|(m, nf)| crate::pandas::WithMetadata::new(*nf, m)),
"id",
flags,
)
}

/// Constraint metadata DataFrame (id-indexed wide format).
Expand Down Expand Up @@ -2127,10 +2153,16 @@ impl Instance {

/// Get a specific named function by ID
pub fn get_named_function_by_id(&self, named_function_id: u64) -> PyResult<NamedFunction> {
let id = NamedFunctionID::from(named_function_id);
self.inner
.named_functions()
.get(&NamedFunctionID::from(named_function_id))
.map(|named_function| NamedFunction(named_function.clone()))
.get(&id)
.map(|named_function| {
NamedFunction(
named_function.clone(),
self.inner.named_function_metadata().collect_for(id),
)
})
.ok_or_else(|| {
PyKeyError::new_err(format!(
"Named function with ID {named_function_id} not found"
Expand Down
29 changes: 21 additions & 8 deletions python/ommx/src/named_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ use ommx::{Evaluate, NamedFunctionID};
use pyo3::{prelude::*, Bound, PyAny};
use std::collections::HashMap;

/// NamedFunction wrapper for Python
/// NamedFunction wrapper for Python.
///
/// Holds the Rust `NamedFunction` plus an owned snapshot of its metadata
/// (`name` / `subscripts` / `parameters` / `description`). The metadata
/// store lives at the host (`Instance` / `Solution` / `SampleSet`) level;
/// when a wrapper is created via a host accessor, the host snapshots its
/// store into the second tuple slot. Mutations on a wrapper do NOT
/// propagate back to the host — re-insert via `Instance.from_components`
/// (or equivalent) to apply changes. Same shape as `Constraint` /
/// `DecisionVariable` after PR #843.
#[pyo3_stub_gen::derive::gen_stub_pyclass]
#[pyclass]
#[derive(Clone)]
pub struct NamedFunction(pub ommx::NamedFunction);
pub struct NamedFunction(pub ommx::NamedFunction, pub ommx::NamedFunctionMetadata);

#[pyo3_stub_gen::derive::gen_stub_pymethods]
#[pymethods]
Expand Down Expand Up @@ -38,13 +47,15 @@ impl NamedFunction {
let named_function = ommx::NamedFunction {
id: named_function_id,
function: rust_function,
};
let metadata = ommx::NamedFunctionMetadata {
name,
subscripts,
parameters: parameters.into_iter().collect(),
description,
};

Ok(Self(named_function))
Ok(Self(named_function, metadata))
}

#[getter]
Expand All @@ -59,22 +70,22 @@ impl NamedFunction {

#[getter]
pub fn name(&self) -> Option<String> {
self.0.name.clone()
self.1.name.clone()
}

#[getter]
pub fn subscripts(&self) -> Vec<i64> {
self.0.subscripts.clone()
self.1.subscripts.clone()
}

#[getter]
pub fn parameters(&self) -> HashMap<String, String> {
self.0.parameters.clone().into_iter().collect()
self.1.parameters.clone().into_iter().collect()
}

#[getter]
pub fn description(&self) -> Option<String> {
self.0.description.clone()
self.1.description.clone()
}

/// Evaluate the named function with the given state.
Expand All @@ -96,7 +107,9 @@ impl NamedFunction {
.0
.evaluate(&state.0, atol)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
Ok(EvaluatedNamedFunction(evaluated))
// Per-element evaluate doesn't see the host's metadata store, so the
// metadata snapshot from the source `NamedFunction` carries over.
Ok(EvaluatedNamedFunction(evaluated, self.1.clone()))
}

/// Partially evaluate the named function with the given state.
Expand Down
61 changes: 37 additions & 24 deletions python/ommx/src/pandas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1262,20 +1262,22 @@ impl<'m> ToPandasEntry
}
}

impl ToPandasEntry for ommx::NamedFunction {
impl<'m> ToPandasEntry for WithMetadata<'m, &ommx::NamedFunction, ommx::NamedFunctionMetadata> {
fn to_pandas_entry<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let nf = self.item;
let m = self.metadata;
let dict = PyDict::new(py);
dict.set_item("id", self.id.into_inner())?;
set_function_type(&dict, &self.function)?;
dict.set_item("function", crate::Function(self.function.clone()))?;
set_used_ids(&dict, &self.function.required_ids())?;
dict.set_item("id", nf.id.into_inner())?;
set_function_type(&dict, &nf.function)?;
dict.set_item("function", crate::Function(nf.function.clone()))?;
set_used_ids(&dict, &nf.function.required_ids())?;
set_metadata(
&dict,
self.name.as_deref(),
&self.subscripts,
self.description.as_deref(),
m.name.as_deref(),
&m.subscripts,
m.description.as_deref(),
)?;
set_parameter_columns(&dict, &self.parameters)?;
set_parameter_columns(&dict, &m.parameters)?;
Ok(dict)
}
}
Expand Down Expand Up @@ -1353,19 +1355,23 @@ impl<'m> ToPandasEntry
}
}

impl ToPandasEntry for ommx::EvaluatedNamedFunction {
impl<'m> ToPandasEntry
for WithMetadata<'m, &ommx::EvaluatedNamedFunction, ommx::NamedFunctionMetadata>
{
fn to_pandas_entry<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let enf = self.item;
let m = self.metadata;
let dict = PyDict::new(py);
dict.set_item("id", self.id.into_inner())?;
dict.set_item("value", self.evaluated_value())?;
set_used_ids(&dict, self.used_decision_variable_ids())?;
dict.set_item("id", enf.id.into_inner())?;
dict.set_item("value", enf.evaluated_value())?;
set_used_ids(&dict, enf.used_decision_variable_ids())?;
set_metadata(
&dict,
self.name.as_deref(),
&self.subscripts,
self.description.as_deref(),
m.name.as_deref(),
&m.subscripts,
m.description.as_deref(),
)?;
set_parameter_columns(&dict, &self.parameters)?;
set_parameter_columns(&dict, &m.parameters)?;
Ok(dict)
}
}
Expand Down Expand Up @@ -1442,20 +1448,27 @@ impl<'a, 'm> ToPandasEntry
}
}

impl<'a> ToPandasEntry for WithSampleIds<'a, &'a ommx::SampledNamedFunction> {
impl<'a, 'm> ToPandasEntry
for WithMetadata<
'm,
WithSampleIds<'a, &'a ommx::SampledNamedFunction>,
ommx::NamedFunctionMetadata,
>
{
fn to_pandas_entry<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let nf = self.item;
let nf = self.item.item;
let m = self.metadata;
let dict = PyDict::new(py);
dict.set_item("id", nf.id().into_inner())?;
set_used_ids(&dict, nf.used_decision_variable_ids())?;
set_metadata(
&dict,
nf.name.as_deref(),
&nf.subscripts,
nf.description.as_deref(),
m.name.as_deref(),
&m.subscripts,
m.description.as_deref(),
)?;
set_parameter_columns(&dict, &nf.parameters)?;
for &sample_id in self.sample_ids {
set_parameter_columns(&dict, &m.parameters)?;
for &sample_id in self.item.sample_ids {
let value = nf.evaluated_values().get(sample_id).copied();
dict.set_item(sample_id.into_inner(), value)?;
}
Expand Down
Loading
Loading