Repository navigation
[ai] Fixes - #1
DavidLapous wants to merge 11 commits into
Conversation
Conversion templates instantiate in consumer extensions, including unoptimized builds where calls cannot disappear through inlining. Expose the conversion implementation symbols from the shared core so strict -O0/-fno-inline consumers can link without hidden-symbol errors.
Python simplex iterables can execute Python while vertices are read. Do not release the GIL around that traversal. Add coverage for list-based lookup and boundary retrieval.
The Python binding passes axis before the vertex filtration value. Match that order in the native signature rather than swapping the two values implicitly through scalar conversions. Cover fractional vertex values and preservation of the untouched axis.
Set the destination tree schema before copying a Slicer. Respect an explicit width override and otherwise inherit the source width, even when no simplices are available to infer it. Cover narrower, wider, and empty conversions.
Integer filtration storage must not truncate real grid coordinates or line parameters before native projection and coarsening. Bind these inputs as double and keep projected values in double. Add integer-tree regressions with fractional grids and non-unit directions.
Native modifiers return the existing tree, not an independently owned wrapper. Use reference_internal for mutation and conversion results so Python retains the original object and its lifetime. Cover normalization identity and mutated filtration values.
Keep a validated grid object even when its axes are empty; clearing the grid remains explicit. Return an empty tuple for an empty degree query. Treat an empty batch filtration as omitted grades after checking CPU storage, preserving the native minus-infinity default instead of parsing an empty matrix as a malformed grade. Cover all three cases.
Infer multi-critical width from a populated lifetime, not an empty first row. Keep the declared width for shaped empty inputs and reject widthless absent lifetimes explicitly. Normalize list and array forms consistently, retaining CPU checks at native ndarray access. Add empty-first, absent, and ragged regressions.
An empty triangulation still has one radius axis plus every function axis. Preserve that width in both native output forms rather than returning default-constructed metadata. Remove the stale Python width setter. Cover scalar and multi-function empty inputs, including tensor grids and gradients.
The timer mutex previously protected an empty block before computation. Hold it around the actual CoreDelaunay operation while the GIL is released, so enabled global timers cannot race between Python threads. Keep empty-input returns outside the native critical section.
Document real projection arithmetic, width overrides, empty metadata, absent versus omitted lifetimes, ragged corners, and input ownership. Retain the existing empty-Slicer limitation explicitly rather than promising inferred metadata for a genuinely empty complex.
| if(NOT MSVC) | ||
| # -fvisibility=hidden matches nanobind's own NB_NAMESPACE hidden-visibility | ||
| # attribute (GCC/Clang only) and silences the resulting -Wattributes. | ||
| # MSVC has no equivalent flag or warning here | ||
| target_compile_options(multipers_core_simplextree_obj PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden) | ||
| endif() |
There was a problem hiding this comment.
If I remember well, this was added to prevent some "declared with greater visibility than its base" and similar warnings. Can you confirm that those are gone and if not find a better solution for them?
| template <class Filtration_value> | ||
| inline Filtration_value _cast_to_filtration_value(nanobind::object values, int defaultNumParam) { | ||
| using value_type = typename Filtration_value::value_type; | ||
| if (nanobind::ndarray<> array; nanobind::try_cast(values, array, false)) _require_cpu_array(array); |
There was a problem hiding this comment.
This test is already done in the ndarray versions of _cast_to_filtration_value. The only possibility for a ndarray to not reach this test is if the ndim > 2 or ndim is 1 and the array is not continuous or U is some exotic type (i.e., only invalid input cases). So, it would be more interesting to add instead something like:
if (nanobind::ndarray<> val; nanobind::try_cast<nanobind::ndarray<>>(values, val, false))
throw std::invalid_argument("Wrong format for filtration value array.");at line 554. It feels more coherent.
| [&]() -> Filtration_value { | ||
| if (_sequence_size(values) != 0) return cast_as_vector(); | ||
| return Filtration_value(defaultNumParam); | ||
| }, |
There was a problem hiding this comment.
If we arrive here the sequence size is supposed to be 0. If it is not the case, then _dispatch_dtype has to be corrected to ensure it. Not here.
| std::vector<Filtration_value> out(view.shape(0), Filtration_value::inf(view.shape(2))); | ||
| if (view.shape(1) == 0) return out; |
There was a problem hiding this comment.
@DavidLapous Can you confirm that we want empty arrays to yield infinity filtration values instead of ones with 0 generators? Or an error message?
|
|
||
| template <class Filtration_value> | ||
| inline std::vector<Filtration_value> _cast_to_filtration_value_array(nanobind::object values, int defaultNumParam) { | ||
| if (nanobind::ndarray<> array; nanobind::try_cast(values, array, false)) _require_cpu_array(array); |
There was a problem hiding this comment.
Same comment than for than for line R554.
| cast_first_as_tensor_then_as_vector, | ||
| []() -> std::vector<Filtration_value> { return {}; }, | ||
| cast_as_vector); | ||
| return detail::_dispatch_dtype(values, cast_first_as_tensor_then_as_vector, cast_as_vector, cast_as_vector); |
There was a problem hiding this comment.
See comment for R658 to R686.
| nanobind::gil_scoped_release release; | ||
| Base::clear(); | ||
| st = build_simplex_tree_from_complex<Options>(other.get_filtered_complex(), maxDim, numParam); | ||
| st.set_num_parameters(numParam >= 0 ? numParam : other.get_number_of_parameters()); |
There was a problem hiding this comment.
I corrected this in build_simplex_tree_from_complex, so it can be removed from here.
| // throws if it does not pass the check | ||
| // returns false if valid but empty | ||
| if (detail::_verify_grid_validity(grid)) { | ||
| filtrationGrid_ = grid; | ||
| return; | ||
| } | ||
|
|
||
| filtrationGrid_ = nanobind::none(); | ||
| detail::_verify_grid_validity(grid); | ||
| filtrationGrid_ = grid; |
There was a problem hiding this comment.
That is not coherent with the behaviour in the Slicer. @DavidLapous can you confirm which version you want?
| nanobind::ndarray<> array; | ||
| if (nanobind::try_cast(filtrationValues, array, false) && array.size() == 0) { | ||
| detail::_require_cpu_array(array); | ||
| } else { | ||
| fils = detail::_cast_to_filtration_value_array<Filtration_value>(filtrationValues, Base::num_parameters()); | ||
| } |
There was a problem hiding this comment.
This test is already done in detail::_cast_to_filtration_value_array, what is the point of the branching? Avoid the machinery of detail::_cast_to_filtration_value_array? Is the runtime gain really worth it?
Also, if the array is empty, what is the point of the test? filtrationValues is never used again afterwards. Then it does not matter if it is cpu or not.
| return self; | ||
| }, | ||
| nb::rv_policy::reference_internal) | ||
| .def("_from_gudhi_state", &Interface::from_std, nb::rv_policy::reference_internal); |
There was a problem hiding this comment.
Is there really a reason to add nb::rv_policy::reference_internal here and everywhere below? If I remember well, when returning a reference from the binded class it-self it is not necessary. Or did I missed something?
Summary
Follow-up fixes for PR68, targeting its source branch
hschreiber/multipers:simplex_tree_interface.The 11 focused commits contain the detailed rationale and implementation changes.
Validation
Validated against PR68 head
838a13b: