git: c4f095e2f2bb - main - science/py-dwave-optimization: update 0.7.1 → 0.7.2

From: Yuri Victorovich <yuri_at_FreeBSD.org>
Date: Tue, 28 Jul 2026 08:23:22 UTC
The branch main has been updated by yuri:

URL: https://cgit.FreeBSD.org/ports/commit/?id=c4f095e2f2bb6cc8e3511ef2827fc7675227b869

commit c4f095e2f2bb6cc8e3511ef2827fc7675227b869
Author:     Yuri Victorovich <yuri@FreeBSD.org>
AuthorDate: 2026-07-28 03:59:50 +0000
Commit:     Yuri Victorovich <yuri@FreeBSD.org>
CommitDate: 2026-07-28 08:23:03 +0000

    science/py-dwave-optimization: update 0.7.1 → 0.7.2
---
 science/py-dwave-optimization/Makefile             | 23 ++++++---
 science/py-dwave-optimization/distinfo             |  6 +--
 .../files/patch-dwave_optimization__model.pyx      | 56 ++++++++++++++++++++++
 ...timization_include_dwave-optimization_array.hpp | 10 ++++
 ...timization_include_dwave-optimization_graph.hpp | 23 +++++++++
 ...n_include_dwave-optimization_nodes_binaryop.hpp | 34 +++++++++++++
 ...ization_include_dwave-optimization_nodes_lp.hpp | 53 ++++++++++++++++++++
 ...ion_include_dwave-optimization_nodes_naryop.hpp | 20 ++++++++
 ...ion_include_dwave-optimization_nodes_reduce.hpp | 22 +++++++++
 ...on_include_dwave-optimization_nodes_unaryop.hpp | 29 +++++++++++
 .../files/patch-dwave_optimization_src_graph.cpp   | 13 +++++
 .../patch-dwave_optimization_src_nodes_lp.cpp      | 20 ++++++++
 .../py-dwave-optimization/files/patch-meson.build  | 28 +++++++++++
 13 files changed, 328 insertions(+), 9 deletions(-)

diff --git a/science/py-dwave-optimization/Makefile b/science/py-dwave-optimization/Makefile
index 6c9df0b43303..813a2a6d4546 100644
--- a/science/py-dwave-optimization/Makefile
+++ b/science/py-dwave-optimization/Makefile
@@ -1,6 +1,5 @@
 PORTNAME=	dwave-optimization
-DISTVERSION=	0.7.1
-PORTREVISION=	3
+DISTVERSION=	0.7.2
 CATEGORIES=	science python # quantum-computing
 MASTER_SITES=	PYPI
 PKGNAMEPREFIX=	${PYTHON_PKGNAMEPREFIX}
@@ -13,19 +12,31 @@ WWW=		https://github.com/dwavesystems/dwave-optimization
 LICENSE=	APACHE20
 LICENSE_FILE=	${WRKSRC}/LICENSE
 
-BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}numpy>=1.16:math/py-numpy@${PY_FLAVOR} \
-		${PYTHON_PKGNAMEPREFIX}meson-python>=0.18.0:devel/meson-python@${PY_FLAVOR}
-RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}numpy>=1.16:math/py-numpy@${PY_FLAVOR}
+BUILD_DEPENDS=	${PYTHON_PKGNAMEPREFIX}meson-python>0:devel/meson-python@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}numpy>=1.21.3:math/py-numpy@${PY_FLAVOR}
+RUN_DEPENDS=	${PYTHON_PKGNAMEPREFIX}numpy>=1.21.3:math/py-numpy@${PY_FLAVOR}
+TEST_DEPENDS=	${PYTHON_PKGNAMEPREFIX}dimod>=0.12.21:science/py-dimod@${PY_FLAVOR} \
+		${PYTHON_PKGNAMEPREFIX}scikit-learn>=1.6.0:science/py-scikit-learn@${PY_FLAVOR}
 
 USES=		python
 USE_PYTHON=	autoplist cython pep517 pytest
 
+PYTEST_BROKEN_TESTS=	test_simple_inputs
+
 post-install:
 	# strip binaries
 	@${STRIP_CMD} \
 		${STAGEDIR}${PYTHON_SITELIBDIR}/dwave/optimization/_model${PYTHON_TAG}.so \
 		${STAGEDIR}${PYTHON_SITELIBDIR}/dwave/optimization/_utilities${PYTHON_TAG}.so \
 		${STAGEDIR}${PYTHON_SITELIBDIR}/dwave/optimization/libdwave-optimization.so \
-		${STAGEDIR}${PYTHON_SITELIBDIR}/dwave/optimization/states${PYTHON_TAG}.so \
+		${STAGEDIR}${PYTHON_SITELIBDIR}/dwave/optimization/states${PYTHON_TAG}.so
+
+pre-test:
+	@cd ${STAGEDIR}${PYTHON_SITELIBDIR}/dwave/optimization && \
+		${FIND} . -name '*.so' -exec ${CP} {} ${WRKSRC}/dwave/optimization/{} \;
+
+# tests as of 0.7.2: 869 passed, 1 broken in 3.23s
+
+# all C++ patches are workaround for https://github.com/dwavesystems/dwave-optimization/issues/595
 
 .include <bsd.port.mk>
diff --git a/science/py-dwave-optimization/distinfo b/science/py-dwave-optimization/distinfo
index ffa3fe497467..a2e87c68b9ae 100644
--- a/science/py-dwave-optimization/distinfo
+++ b/science/py-dwave-optimization/distinfo
@@ -1,3 +1,3 @@
-TIMESTAMP = 1781738751
-SHA256 (dwave_optimization-0.7.1.tar.gz) = 70085ae160b7692225392650924f137bc2c74f405d1a45d4e011aa647cb3ec03
-SIZE (dwave_optimization-0.7.1.tar.gz) = 520638
+TIMESTAMP = 1785188115
+SHA256 (dwave_optimization-0.7.2.tar.gz) = c8bd908c4714a0036a8710ad27840b5226c3b3cdd9f85f5176149b7db3a4ace3
+SIZE (dwave_optimization-0.7.2.tar.gz) = 535092
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization__model.pyx b/science/py-dwave-optimization/files/patch-dwave_optimization__model.pyx
new file mode 100644
index 000000000000..352cb53479e2
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization__model.pyx
@@ -0,0 +1,56 @@
+-- Use the mangled C++ type name as the runtime type registry key instead of
+-- std::type_index.  On FreeBSD/Clang libc++ compares type_info object addresses
+-- by default, and the same class template instantiations are emitted into both
+-- libdwave-optimization.so and each Cython extension module.  The resulting
+-- distinct type_info objects for the same type cause registry lookups to fail,
+-- producing "given pointer cannot be cast to a known node type".  Comparing
+-- mangled type name strings works across DSO boundaries.
+
+--- dwave/optimization/_model.pyx.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/_model.pyx
+@@ -32,7 +32,7 @@
+ from libcpp cimport bool
+ from libcpp.memory cimport make_shared
+ from libcpp.span cimport span
+-from libcpp.typeindex cimport type_index
++from libcpp.string cimport string
+ from libcpp.unordered_map cimport unordered_map
+ from libcpp.utility cimport move
+ from libcpp.vector cimport vector
+@@ -65,17 +65,22 @@
+ """A tuple of 2-tuples listing all serialization versions supported."""
+
+
+-# Store a mapping from the type_index of each C++ Node type to the relevant
+-# Cython class. We don't refcount the PyObject*s pointing to each class because
+-# the lifespace of this map is identical to that of the type objects.
+-cdef unordered_map[type_index, PyObject*] _cpp_type_to_python
++# Store a mapping from the mangled C++ type name of each Node type to the
++# relevant Cython class. We use the type name string rather than std::type_index
++# because libc++'s default type_info comparison compares object addresses, and
++# the same C++ class template instantiations are emitted into both
++# libdwave-optimization.so and the individual Cython extension modules, giving
++# distinct type_info objects for the same type across DSOs on FreeBSD/Clang.
++# We don't refcount the PyObject*s pointing to each class because the lifespan of
++# this map is identical to that of the type objects.
++cdef unordered_map[string, PyObject*] _cpp_type_to_python
+
+ # Register a mapping between the given Cython class and C++ class.
+ cdef void _register(object cls, const type_info& typeinfo):
+     """Register a Python/Cython symbol to allow it to be created from a pointer
+     via `symbol_from_ptr`.
+     """
+-    _cpp_type_to_python[type_index(typeinfo)] = <PyObject*>(cls)
++    _cpp_type_to_python[string(typeinfo.name())] = <PyObject*>(cls)
+
+
+ cdef object symbol_from_ptr(_Graph model, cppNode* node_ptr):
+@@ -86,7 +91,7 @@
+         raise ValueError("cannot construct a Symbol from the given pointer")
+
+     try:
+-        cls = <object>_cpp_type_to_python.at(type_index(typeid(deref(node_ptr))))
++        cls = <object>_cpp_type_to_python.at(string(typeid(deref(node_ptr)).name()))
+     except IndexError:
+         # IndexError would be returned by .at()
+         raise RuntimeError("given pointer cannot be cast to a known node type") from None
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_array.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_array.hpp
new file mode 100644
index 000000000000..f56826b81f7c
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_array.hpp
@@ -0,0 +1,10 @@
+--- dwave/optimization/include/dwave-optimization/array.hpp.orig	2026-07-27 22:30:07 UTC
++++ dwave/optimization/include/dwave-optimization/array.hpp
+@@ -371,6 +371,7 @@ class Array {
+ /// the Array::dynamic() method is provided.
+ class Array {
+  public:
++    virtual ~Array();
+     /// A std::random_access_iterator over the values in the array.
+     using iterator = BufferIterator<double, double>;
+ 
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_graph.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_graph.hpp
new file mode 100644
index 000000000000..a942ca5ccb89
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_graph.hpp
@@ -0,0 +1,23 @@
+--- dwave/optimization/include/dwave-optimization/graph.hpp.orig	2026-07-27 22:30:07 UTC
++++ dwave/optimization/include/dwave-optimization/graph.hpp
+@@ -254,7 +254,7 @@ class Node {
+     };
+ 
+     Node() noexcept : expired_ptr_(new bool(false)) {}
+-    virtual ~Node() { *expired_ptr_ = true; }
++    virtual ~Node();
+ 
+     // Nodes cannot be moved or copied.
+     Node(const Node&) = delete;
+@@ -452,7 +452,10 @@ NodeType* Graph::emplace_node(Args&&... args) {
+     return ptr;  // return the observing pointer
+ }
+ 
+-class ArrayNode : public Array, public virtual Node {};
++class ArrayNode : public Array, public virtual Node {
++ public:
++    ~ArrayNode() override;
++};
+ class DecisionNode : public Decision, public virtual Node {
+  public:
+     /// Decision nodes by definition do not have a deterministic state.
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_binaryop.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_binaryop.hpp
new file mode 100644
index 000000000000..6f107c0cecb0
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_binaryop.hpp
@@ -0,0 +1,34 @@
+-- Declare explicit template instantiations for BinaryOpNode specializations so
+-- they are defined only in libdwave-optimization.so (dwave/optimization/src/nodes/binaryop.cpp).
+-- Without this, each Cython extension module that creates a binary-op node
+-- instantiates its own copy of the class template, producing a distinct type_info
+-- object for the same type.  libc++'s default type_info comparison then treats
+-- those copies as different types and cross-DSO dynamic_cast fails on
+-- FreeBSD/Clang.
+
+--- dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp
+@@ -106,4 +106,23 @@
+ using SubtractNode = BinaryOpNode<std::minus<double>>;
+ using XorNode = BinaryOpNode<functional::logical_xor<double>>;
+
++// Explicit instantiations live in dwave/optimization/src/nodes/binaryop.cpp so
++// that libdwave-optimization.so owns the canonical type_info objects. Tell
++// other translation units (especially the Cython extension modules) not to
++// emit their own copies, which would break cross-DSO dynamic_cast on
++// FreeBSD/Clang.
++extern template class BinaryOpNode<std::plus<double>>;
++extern template class BinaryOpNode<std::logical_and<double>>;
++extern template class BinaryOpNode<std::divides<double>>;
++extern template class BinaryOpNode<std::equal_to<double>>;
++extern template class BinaryOpNode<std::less_equal<double>>;
++extern template class BinaryOpNode<functional::max<double>>;
++extern template class BinaryOpNode<functional::min<double>>;
++extern template class BinaryOpNode<functional::modulus<double>>;
++extern template class BinaryOpNode<std::multiplies<double>>;
++extern template class BinaryOpNode<std::logical_or<double>>;
++extern template class BinaryOpNode<functional::safe_divides<double>>;
++extern template class BinaryOpNode<std::minus<double>>;
++extern template class BinaryOpNode<functional::logical_xor<double>>;
++
+ }  // namespace dwave::optimization
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_lp.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_lp.hpp
new file mode 100644
index 000000000000..55123382dbfd
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_lp.hpp
@@ -0,0 +1,53 @@
+-- Make the LP node destructors out-of-line so their type_info objects are
+-- emitted as strong (GLOBAL) symbols in libdwave-optimization.so.  Without
+-- this, the Cython lp.so module emits its own copies of the polymorphic base
+-- LinearProgramNodeBase type_info, which breaks cross-DSO dynamic_cast from
+-- LinearProgramNode to LinearProgramNodeBase on FreeBSD/Clang.
+
+--- dwave/optimization/include/dwave-optimization/nodes/lp.hpp.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/include/dwave-optimization/nodes/lp.hpp
+@@ -27,6 +27,8 @@
+ /// A logical node that propagates whether or not its predecessor LinearProgram is feasible.
+ class LinearProgramFeasibleNode : public ScalarOutputMixin<EqualityMixin<ArrayNode>, true> {
+  public:
++    virtual ~LinearProgramFeasibleNode();
++
+     explicit LinearProgramFeasibleNode(LinearProgramNodeBase* lp_ptr);
+
+     /// @copydoc Node::initialize_state()
+@@ -53,6 +55,8 @@
+
+ class LinearProgramNodeBase : public Node {
+  public:
++    virtual ~LinearProgramNodeBase();
++
+     /// The default lower bound for variables
+     static const double default_lower_bound();
+
+@@ -102,6 +106,8 @@
+ ///         callback=None, options=None, x0=None, integrality=None)
+ class LinearProgramNode : public EqualityMixin<LinearProgramNodeBase, LinearProgramNode> {
+  public:
++    virtual ~LinearProgramNode();
++
+     /// Construct a LinearProgramNode
+     ///
+     /// Note: parameter names are chosen to match scipy.optimize.lingprog()
+@@ -194,6 +200,8 @@
+ /// LinearProgramNode. Note that the output is undefined if the solution is not feasible.
+ class LinearProgramObjectiveValueNode : public ScalarOutputMixin<EqualityMixin<ArrayNode>, true> {
+  public:
++    virtual ~LinearProgramObjectiveValueNode();
++
+     explicit LinearProgramObjectiveValueNode(LinearProgramNodeBase* lp_ptr);
+
+     /// @copydoc Node::initialize_state()
+@@ -222,6 +230,8 @@
+ /// solution may not be feasible or optimial.
+ class LinearProgramSolutionNode : public ArrayOutputMixin<EqualityMixin<ArrayNode>> {
+  public:
++    virtual ~LinearProgramSolutionNode();
++
+     explicit LinearProgramSolutionNode(LinearProgramNodeBase* lp_ptr);
+
+     /// @copydoc Array::buff()
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_naryop.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_naryop.hpp
new file mode 100644
index 000000000000..0f8d99f6a4e8
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_naryop.hpp
@@ -0,0 +1,20 @@
+-- Declare explicit template instantiations for NaryOpNode specializations so
+-- they are defined only in libdwave-optimization.so (dwave/optimization/src/nodes/naryop.cpp).
+-- This prevents each Cython extension module from emitting its own type_info
+-- copy, which breaks cross-DSO dynamic_cast on FreeBSD/Clang.
+
+--- dwave/optimization/include/dwave-optimization/nodes/naryop.hpp.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/include/dwave-optimization/nodes/naryop.hpp
+@@ -86,4 +86,12 @@
+ using NaryMinimumNode = NaryOpNode<functional::min<double>>;
+ using NaryMultiplyNode = NaryOpNode<std::multiplies<double>>;
+
++// Explicit instantiations live in dwave/optimization/src/nodes/naryop.cpp so
++// that libdwave-optimization.so owns the canonical type_info objects. Tell
++// other translation units not to emit their own copies.
++extern template class NaryOpNode<functional::max<double>>;
++extern template class NaryOpNode<functional::min<double>>;
++extern template class NaryOpNode<std::multiplies<double>>;
++extern template class NaryOpNode<std::plus<double>>;
++
+ }  // namespace dwave::optimization
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_reduce.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_reduce.hpp
new file mode 100644
index 000000000000..5c1f97e00436
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_reduce.hpp
@@ -0,0 +1,22 @@
+-- Declare explicit template instantiations for ReduceNode specializations so
+-- they are defined only in libdwave-optimization.so (dwave/optimization/src/nodes/reduce.cpp).
+-- This prevents each Cython extension module from emitting its own type_info
+-- copy, which breaks cross-DSO dynamic_cast on FreeBSD/Clang.
+
+--- dwave/optimization/include/dwave-optimization/nodes/reduce.hpp.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/include/dwave-optimization/nodes/reduce.hpp
+@@ -141,4 +141,14 @@
+ using ProdNode = ReduceNode<std::multiplies<double>>;
+ using SumNode = ReduceNode<std::plus<double>>;
+
++// Explicit instantiations live in dwave/optimization/src/nodes/reduce.cpp so
++// that libdwave-optimization.so owns the canonical type_info objects. Tell
++// other translation units not to emit their own copies.
++extern template class ReduceNode<functional::max<double>>;
++extern template class ReduceNode<functional::min<double>>;
++extern template class ReduceNode<std::logical_and<double>>;
++extern template class ReduceNode<std::logical_or<double>>;
++extern template class ReduceNode<std::multiplies<double>>;
++extern template class ReduceNode<std::plus<double>>;
++
+ }  // namespace dwave::optimization
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_unaryop.hpp b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_unaryop.hpp
new file mode 100644
index 000000000000..45873043e3e2
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_include_dwave-optimization_nodes_unaryop.hpp
@@ -0,0 +1,29 @@
+-- Declare explicit template instantiations for UnaryOpNode specializations so
+-- they are defined only in libdwave-optimization.so (dwave/optimization/src/nodes/unaryop.cpp).
+-- This prevents each Cython extension module from emitting its own type_info
+-- copy, which breaks cross-DSO dynamic_cast on FreeBSD/Clang.
+
+--- dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp
+@@ -96,4 +96,21 @@
+ using SquareRootNode = UnaryOpNode<functional::square_root<double>>;
+ using TanhNode = UnaryOpNode<functional::tanh<double>>;
+
++// Explicit instantiations live in dwave/optimization/src/nodes/unaryop.cpp so
++// that libdwave-optimization.so owns the canonical type_info objects. Tell
++// other translation units not to emit their own copies.
++extern template class UnaryOpNode<functional::abs<double>>;
++extern template class UnaryOpNode<functional::cos<double>>;
++extern template class UnaryOpNode<functional::exp<double>>;
++extern template class UnaryOpNode<functional::expit<double>>;
++extern template class UnaryOpNode<functional::log<double>>;
++extern template class UnaryOpNode<functional::logical<double>>;
++extern template class UnaryOpNode<functional::rint<double>>;
++extern template class UnaryOpNode<functional::sin<double>>;
++extern template class UnaryOpNode<functional::square<double>>;
++extern template class UnaryOpNode<functional::square_root<double>>;
++extern template class UnaryOpNode<functional::tanh<double>>;
++extern template class UnaryOpNode<std::negate<double>>;
++extern template class UnaryOpNode<std::logical_not<double>>;
++
+ }  // namespace dwave::optimization
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_src_graph.cpp b/science/py-dwave-optimization/files/patch-dwave_optimization_src_graph.cpp
new file mode 100644
index 000000000000..1dba5bb43e27
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_src_graph.cpp
@@ -0,0 +1,13 @@
+--- dwave/optimization/src/graph.cpp.orig	2026-07-27 22:30:07 UTC
++++ dwave/optimization/src/graph.cpp
+@@ -33,6 +33,10 @@ namespace dwave::optimization {
+ 
+ namespace dwave::optimization {
+ 
++Node::~Node() { *expired_ptr_ = true; }
++Array::~Array() = default;
++ArrayNode::~ArrayNode() = default;
++
+ void Graph::add_constraint(ArrayNode* constraint_ptr) {
+     if (not constraint_ptr->logical()) {
+         throw std::invalid_argument("constraint must have a logical output");
diff --git a/science/py-dwave-optimization/files/patch-dwave_optimization_src_nodes_lp.cpp b/science/py-dwave-optimization/files/patch-dwave_optimization_src_nodes_lp.cpp
new file mode 100644
index 000000000000..72aae7804fb8
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-dwave_optimization_src_nodes_lp.cpp
@@ -0,0 +1,20 @@
+-- Define out-of-line destructors for the LP node classes so their type_info
+-- objects are emitted as strong (GLOBAL) symbols in libdwave-optimization.so,
+-- matching the header declarations.  This fixes cross-DSO dynamic_cast failures
+-- on FreeBSD/Clang where each shared library otherwise gets its own copy of the
+-- base LinearProgramNodeBase type_info.
+
+--- dwave/optimization/src/nodes/lp.cpp.orig	2026-07-23 12:58:12 UTC
++++ dwave/optimization/src/nodes/lp.cpp
+@@ -22,6 +22,12 @@
+ #include "dwave-optimization/graph.hpp"
+
+ namespace dwave::optimization {
++
++LinearProgramFeasibleNode::~LinearProgramFeasibleNode() = default;
++LinearProgramNodeBase::~LinearProgramNodeBase() = default;
++LinearProgramNode::~LinearProgramNode() = default;
++LinearProgramObjectiveValueNode::~LinearProgramObjectiveValueNode() = default;
++LinearProgramSolutionNode::~LinearProgramSolutionNode() = default;
+
+ static constexpr double FEASIBILITY_TOLERANCE = 1e-07;
diff --git a/science/py-dwave-optimization/files/patch-meson.build b/science/py-dwave-optimization/files/patch-meson.build
new file mode 100644
index 000000000000..42ee1f84c6ee
--- /dev/null
+++ b/science/py-dwave-optimization/files/patch-meson.build
@@ -0,0 +1,28 @@
+-- Force libc++ to use name-based type_info comparison.  On FreeBSD/Clang the
+-- default compares type_info object addresses, but the same C++ class
+-- template instantiations are emitted into both libdwave-optimization.so and
+-- each Cython extension module, producing distinct type_info objects for the
+-- same type.  This breaks cross-DSO std::type_index lookups and dynamic_cast.
+-- The "non-unique" implementation compares mangled type names, so types match
+-- across shared libraries.
+
+--- meson.build.orig	2026-07-23 19:58:12 UTC
++++ meson.build
+@@ -22,6 +22,17 @@ add_project_arguments(['-g1'], language: ['cpp'])
+ # We want some debugging symbols
+ add_project_arguments(['-g1'], language: ['cpp'])
+ 
++# On FreeBSD/Clang libc++ defaults to comparing type_info object addresses. The
++# same C++ class template instantiations are emitted into both
++# libdwave-optimization.so and the individual Cython extension modules, so
++# each shared object gets its own type_info copy and cross-DSO dynamic_cast
++# and std::type_index lookups fail. Force the "non-unique" implementation,
++# which compares mangled type names, so types match across shared libraries.
++add_project_arguments(
++    '-D_LIBCPP_TYPEINFO_COMPARISON_IMPLEMENTATION=2',
++    language: 'cpp',
++)
++
+ cpp = meson.get_compiler('cpp')
+ py = import('python').find_installation(pure: false)
+