sdot 2024.11.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. sdot-2024.11.23.0/PKG-INFO +79 -0
  2. sdot-2024.11.23.0/README.md +1 -0
  3. sdot-2024.11.23.0/pyproject.toml +22 -0
  4. sdot-2024.11.23.0/sdot/Cell.py +272 -0
  5. sdot-2024.11.23.0/sdot/ConvexFunction.py +199 -0
  6. sdot-2024.11.23.0/sdot/D2GTransportMap.py +5 -0
  7. sdot-2024.11.23.0/sdot/Expr.py +142 -0
  8. sdot-2024.11.23.0/sdot/G2DTransportMap.py +6 -0
  9. sdot-2024.11.23.0/sdot/PoomVec.py +92 -0
  10. sdot-2024.11.23.0/sdot/PowerDiagram.py +376 -0
  11. sdot-2024.11.23.0/sdot/PowerDiagramSummary.py +63 -0
  12. sdot-2024.11.23.0/sdot/SdotSolver.py +297 -0
  13. sdot-2024.11.23.0/sdot/TransformationMatrix.py +46 -0
  14. sdot-2024.11.23.0/sdot/VtkOutput.py +18 -0
  15. sdot-2024.11.23.0/sdot/__init__.py +18 -0
  16. sdot-2024.11.23.0/sdot/bindings/RefMap.h +101 -0
  17. sdot-2024.11.23.0/sdot/bindings/TF_objects.SConstruct +8 -0
  18. sdot-2024.11.23.0/sdot/bindings/TF_objects_bindings.cpp +16 -0
  19. sdot-2024.11.23.0/sdot/bindings/__init__.py +0 -0
  20. sdot-2024.11.23.0/sdot/bindings/construct.py +158 -0
  21. sdot-2024.11.23.0/sdot/bindings/ext +1 -0
  22. sdot-2024.11.23.0/sdot/bindings/generated/1jFP64200/integration_1jFP64200.cpp +0 -0
  23. sdot-2024.11.23.0/sdot/bindings/generated/1l/integration_1l.SConstruct +4 -0
  24. sdot-2024.11.23.0/sdot/bindings/generated/1l/integration_1l.cpp +51 -0
  25. sdot-2024.11.23.0/sdot/bindings/generic_objects.SConstruct +8 -0
  26. sdot-2024.11.23.0/sdot/bindings/generic_objects_bindings.cpp +84 -0
  27. sdot-2024.11.23.0/sdot/bindings/img_interpolation.SConstruct +8 -0
  28. sdot-2024.11.23.0/sdot/bindings/img_interpolation_bindings.cpp +35 -0
  29. sdot-2024.11.23.0/sdot/bindings/integration.SConstruct +14 -0
  30. sdot-2024.11.23.0/sdot/bindings/integration_bindings.cpp +48 -0
  31. sdot-2024.11.23.0/sdot/bindings/integration_module.py +105 -0
  32. sdot-2024.11.23.0/sdot/bindings/loader.py +156 -0
  33. sdot-2024.11.23.0/sdot/bindings/poom_vec_nd_0.SConstruct +8 -0
  34. sdot-2024.11.23.0/sdot/bindings/poom_vec_nd_0_bindings.cpp +46 -0
  35. sdot-2024.11.23.0/sdot/bindings/poom_vec_nd_1.SConstruct +8 -0
  36. sdot-2024.11.23.0/sdot/bindings/poom_vec_nd_1_bindings.cpp +49 -0
  37. sdot-2024.11.23.0/sdot/bindings/sdot.SConstruct +6 -0
  38. sdot-2024.11.23.0/sdot/bindings/sdot_bindings.cpp +345 -0
  39. sdot-2024.11.23.0/sdot/bindings/src +1 -0
  40. sdot-2024.11.23.0/sdot/bindings/variants/integration_module/1l/integration_module.cpp +1 -0
  41. sdot-2024.11.23.0/sdot/distributions/Distribution.py +21 -0
  42. sdot-2024.11.23.0/sdot/distributions/SumOfDiracs.py +34 -0
  43. sdot-2024.11.23.0/sdot/distributions/SymbolicFunction.py +17 -0
  44. sdot-2024.11.23.0/sdot/distributions/UnitBox.py +41 -0
  45. sdot-2024.11.23.0/sdot/distributions/normalized_distribution.py +23 -0
  46. sdot-2024.11.23.0/sdot/optimal_transport_plan.py +9 -0
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.3
2
+ Name: sdot
3
+ Version: 2024.11.23.0
4
+ Summary: Tools to construct/handle PowerDiagram, notably for semi-discrete optimal transport
5
+ Keywords: optimal transport,power diagram,Laguerre diagram
6
+ Author-email: Hugo Leclerc <hugo.leclerc@universite-paris-saclay.fr>
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: munch
9
+ Requires-Dist: pybind11
10
+ Requires-Dist: archspec
11
+ Requires-Dist: scons
12
+ Requires-Dist: pytest
13
+ Requires-Dist: numpy
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: dload
16
+ Requires-Dist: gitpython
17
+
18
+ SDOT
19
+ ====
20
+
21
+ This package contains tool to handle
22
+ * semi-discrete transport plans (between discrete and generic densities),
23
+ * polyhedral convex functions (represented as a max of affine functions),
24
+ * and power diagrams (that can be seen as a generalization of Voronoi diagrams).
25
+
26
+ It works in any number of dimensions, and is highly optimized, in terms of execution speed and memory usage.
27
+
28
+ Historically, this package is a re-design of the `pysdot` package, which was written to handle a wide number of semi-discrete transport applications (partial transport, Moreau-Yosida regularization, etc...) but only worked in 2D or 3D. Additionaly, and more specifically, we wanted to make operations on power diagram much more comprehensive and generic.
29
+
30
+ Currently, there are bindings C++ and Python, but more languages are in the pipeline (feel free to ask if your favorite language is not represented :) ).
31
+
32
+ Installation
33
+ ------------
34
+
35
+ ### Pip
36
+
37
+ For python, `pip install sdot` should do the job, including some precompiled libraries for the most common cases (2D/3D, float64, ...). If your cases are not included in the distribution, the required dynamic libraries will be automatically compiled on first use. In this situation, you will need to have a C++ compiler installed on your machine. As scons is used to find and call the compiler, all you need to do is install one compiler that is compatible with this builder (for instance g++, clang, xcode, vscode, ...).
38
+
39
+ For a compiler : under Debian like, `sudo apt install g++`. Under Mac os, `xcode-select --install`. Under Windows, you can follow [this link](https://code.visualstudio.com/docs/cpp/config-mingw#_prerequisites).
40
+
41
+ ### Sources
42
+
43
+ To get that latest version, `sdot` can also be installed from the git repository.
44
+
45
+ For the python modules:
46
+
47
+ ```bash
48
+ git clone https://github.com/sdot-team/sdot.git
49
+ # maybe after a micromamba activate ...
50
+ cd sdot/src/python
51
+ pip install flit
52
+ flit install -s # -s makes symbolic links to the sources
53
+ ```
54
+
55
+ Modules
56
+ -------
57
+
58
+ * [Optimal transport operations in python](doc/optimal_transport_py.md).
59
+ * [Power or voronoi diagrams and cells in python](doc/power_diagram_py.md).
60
+ * [Polyhedral convex function in python](doc/polyhedral_convex_py.md).
61
+
62
+ A word on performance
63
+ ---------------------
64
+
65
+ The most common tools to handle voronoi and power diagrams start from delaunay (regular) triangulations. Building this triangulation is generaly the most time-consuming part of this approach, notably because one have to deal with the problems that come with digital precision...
66
+
67
+ Nevertheless, for most applications of the sdot package, we only need the *integrals* of the cells and the boundaries, meaning that most of the problems with digital precision naturally vanish at the end. We also realized that it was much more convenient for the user to work directly with the cells.
68
+
69
+ Bearing in mind that exact connectivity may of course be required for some applications (the 'dual' geometry becoming the triangulation in our case :) ), we therefore decided to give a try to algorithms with a focus on the cells, that are computed individually in a fully parallel fashion. We tried to stay on the user specified scalar types (e.g. `float64`) as long as possible. Finally, we designed adapted spatial acceleration structures to stay within O(n log(n)) execution speed with the smallest possible constant.
70
+
71
+ Internally, it is written in C++/Cuda with SIMD/SIMT instructions. It support large vectors, for out-of-core and multi-machine computations. More on [this page](doc/performance.md).
72
+
73
+ On going work
74
+ -------------
75
+
76
+ * pytorch compatible operations
77
+ * pre-guess of the weight to avoid void cells at the beginning
78
+ * non-linear solvers to avoid bad newton directions
79
+
@@ -0,0 +1 @@
1
+ /home/leclerc/sdot/README.md
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["flit_core >=3.2,<4","pytest"]
3
+ build-backend = "flit_core.buildapi"
4
+
5
+ [project]
6
+ description = "Tools to construct/handle PowerDiagram, notably for semi-discrete optimal transport"
7
+ authors = [ { name = "Hugo Leclerc", email = "hugo.leclerc@universite-paris-saclay.fr" }, ]
8
+ keywords = ["optimal transport","power diagram","Laguerre diagram"]
9
+ dependencies = [ "munch", "pybind11", "archspec", "scons", "pytest", "numpy", "matplotlib", "dload", "gitpython" ]
10
+ version = "2024.11.23.0"
11
+ readme = "README.md"
12
+ name = "sdot"
13
+
14
+ # Documentation = "https://readthedocs.org"
15
+ # Repository = "https://github.com/me/spam.git"
16
+
17
+ # [tool.hatch.build.targets.wheel]
18
+ # artifacts = [
19
+ # "*.so",
20
+ # "*.dll",
21
+ # "*.dylib",
22
+ # ]
@@ -0,0 +1,272 @@
1
+ from .bindings.integration_module import integration_module
2
+ from .bindings.loader import module_for
3
+ from .Expr import Expr
4
+
5
+ from types import ModuleType
6
+ import numpy as np
7
+
8
+ class Cell:
9
+ """ wrapper around cpp sdot::Cell class """
10
+
11
+ def __init__( self, ndim = None, dtype = None, _cell = None, _binding_module = None ):
12
+ """ _cell is the sdot::Cell class instance. If not specified, __init__ makes a new one using `ndim` and `dtype`.
13
+ By default, dtype = np.double, ndim = 2
14
+ """
15
+
16
+ if _cell:
17
+ assert _binding_module is not None
18
+ self._binding_module = _binding_module
19
+ self._cell = _cell
20
+ return
21
+
22
+ if ndim is None:
23
+ ndim = 2
24
+
25
+ if dtype is None:
26
+ dtype = "FP64"
27
+
28
+ self._binding_module = module_for( 'sdot', use_arch = True, scalar_type = dtype, nb_dims = ndim )
29
+ self._cell = self._binding_module.Cell()
30
+
31
+ def __repr__( self ):
32
+ return self._cell.__repr__()
33
+
34
+ def integral( self, funcs = '1', underlying_measure_override = None, underlying_radial_function_override = None ):
35
+ """ integral of a symbolic function or a list of symbolic functions """
36
+ if not isinstance( funcs, list ):
37
+ return self.integral( [ funcs ], underlying_measure_override, underlying_radial_function_override )[ 0 ]
38
+
39
+ # module généré
40
+ module, rt_data = integration_module( funcs, self._binding_module.dtype(), self._binding_module.ndim() )
41
+ return module.cell_integral( self._cell, rt_data )
42
+
43
+ @property
44
+ def true_dimensionality( self ):
45
+ """ Size of the base.
46
+
47
+ at the beginning, all the cell starts with true_dimensionality == 0. A first cut will lead to true_dimensionality == 1.
48
+ If not in the same direction, true_dimensionality will be incremented, ...
49
+ If true_dimensionality < ndims, coordinates are expressed in self.base
50
+ """
51
+ return self._cell.true_dimensionality()
52
+
53
+ @property
54
+ def vertex_coords_td( self ):
55
+ """ get list of coordinates for each vertex (stored in a numpy array) in the current base """
56
+ return self._cell.vertex_coords( True )
57
+
58
+ @property
59
+ def vertex_coords( self ):
60
+ """ get list of coordinates for each vertex (stored in a numpy array) """
61
+ return self._cell.vertex_coords( False )
62
+
63
+ @property
64
+ def vertex_refs_td( self ):
65
+ """ get list of cut indices for each vertex (stored in a numpy array) in the current base """
66
+ return self._cell.vertex_refs( True )
67
+
68
+ @property
69
+ def vertex_refs( self ):
70
+ """ get list of cut indices for each vertex (stored in a numpy array) in the current base """
71
+ return self._cell.vertex_refs( False )
72
+
73
+ @property
74
+ def nb_active_cuts( self ):
75
+ """ beware: if remove_inactive_cuts() has not been called, this procedure will have first to compute the active cuts (may take some time) """
76
+ return self._cell.nb_active_cuts()
77
+
78
+ @property
79
+ def nb_stored_cuts( self ):
80
+ """ may be different from nb_active_cuts if remove_inactive_cuts() has not been called """
81
+ return self._cell.nb_stored_cuts()
82
+
83
+ @property
84
+ def nb_vertices_td( self ):
85
+ """ nb vertices in current "true" dimensionality """
86
+ return self._cell.nb_vertices( True )
87
+
88
+ @property
89
+ def nb_vertices( self ):
90
+ return self._cell.nb_vertices( False )
91
+
92
+ @property
93
+ def bounded( self ):
94
+ return self._cell.bounded()
95
+
96
+ @property
97
+ def empty( self ):
98
+ return self._cell.empty()
99
+
100
+ # @property
101
+ # def closed_faces( self ):
102
+ # return self._cell.closed_faces()
103
+
104
+ @property
105
+ def base( self ):
106
+ """ internal base, used to get coordinates and directions if true_dimensionnality < nb_dims.
107
+ It true_dimensionality == nb_dims, it return an identity matrix
108
+ """
109
+ return self._cell.base()
110
+
111
+ @property
112
+ def ndim( self ):
113
+ return self._cell.ndim()
114
+
115
+ @property
116
+ def cuts( self ):
117
+ return self._cell.cuts()
118
+
119
+ def cut_boundary( self, dir, val, index = 0 ):
120
+ return self._cell.cut_boundary( np.ascontiguousarray( dir ), val, int( index ) )
121
+
122
+ def cut( self, dir, val, index = 0 ):
123
+ return self._cell.cut( np.ascontiguousarray( dir ), val, int( index ) )
124
+
125
+ def display_vtk( self, vtk_output ):
126
+ return self._cell.display_vtk( vtk_output )
127
+
128
+ # def for_each_face( self, on_face ):
129
+ # """
130
+ # Call args:
131
+ # on_face( cut_refs, vertex_indices, ray_refs_list ),
132
+ # """
133
+ # return self._cell.for_each_face( on_face )
134
+
135
+ def for_each_edge( self, on_edge ):
136
+ """
137
+ on_edge( edge_refs, vertex_indices )
138
+ * edge_refs = [ num_cut_0, ... ]
139
+ * vertex_indices =>
140
+ - size = 2 for a regular edge
141
+ - size = 1 for a ray
142
+
143
+ """
144
+ return self._cell.for_each_edge( on_edge, False )
145
+
146
+ def for_each_edge_td( self, on_edge ):
147
+ """
148
+ on_edge( edge_refs, vertex_indices )
149
+ * edge_refs = [ num_cut_0, ... ]
150
+ * vertex_indices =>
151
+ - size = 2 for a regular edge
152
+ - size = 1 for a ray
153
+
154
+ """
155
+ return self._cell.for_each_edge( on_edge, True )
156
+
157
+ def ray_dir( self, ray_refs, base_vertex ):
158
+ return self._cell.ray_dir( ray_refs, base_vertex )
159
+
160
+ def plot_pyplot( self, fig, color = 'black', linestyle = '-', linewidth = 2, low_dim_linewidth_coeff = 0.25, ray_length = 0.2, ray_color = None, ray_linestyle = '--', free_color = None, free_linestyle = ':' ):
161
+ """
162
+ ray_length is the length used to display infinite edges.
163
+ """
164
+
165
+ # check fig properties
166
+ if isinstance( fig, ModuleType ):
167
+ fig.axis( 'equal' )
168
+
169
+ if self.ndim == 3:
170
+ fig = fig.figure()
171
+ fig = fig.add_subplot( projection = '3d' )
172
+
173
+ #
174
+ coords = self.vertex_coords_td @ self.base
175
+
176
+ # find a complementary base
177
+ cbase = list( np.eye( self.ndim ) )
178
+ for r in range( self.ndim ):
179
+ for c in self.base:
180
+ cbase[ r ] -= np.dot( cbase[ r ], c ) * c
181
+ cbase.sort( key = np.linalg.norm )
182
+ cbase = cbase[ self.true_dimensionality : ]
183
+
184
+ # helper to plot
185
+ def pl( array, gtype ):
186
+ array = np.array( array )
187
+ xyz = [ array[ :, d ] for d in range( min( self.ndim, 3 ) ) ]
188
+ kwa = { 'linewidth': linewidth }
189
+ if self.true_dimensionality < self.ndim and gtype != 'free':
190
+ kwa[ 'linewidth' ] *= low_dim_linewidth_coeff
191
+
192
+ if gtype == 'free':
193
+ return fig.plot( *xyz, **kwa, linestyle = free_linestyle, color = free_color or color )
194
+ if gtype == 'ray':
195
+ return fig.plot( *xyz, **kwa, linestyle = ray_linestyle, color = ray_color or color )
196
+ if gtype == 'edge':
197
+ return fig.plot( *xyz, **kwa, linestyle = linestyle, color = color )
198
+
199
+ # for each vertex, plot free items, get the edges and the rays
200
+ for c in coords:
201
+ for cvec in cbase:
202
+ pl( [ c - ray_length * cvec, c + ray_length * cvec ], 'free' )
203
+
204
+ # plot edge
205
+ def edge_func( refs, vertices ):
206
+ if len( refs ) == 0:
207
+ return
208
+
209
+ # ray
210
+ if len( vertices ) == 1:
211
+ rd = self.ray_dir( refs, vertices[ 0 ] ) @ self.base
212
+ rd /= np.linalg.norm( rd )
213
+ bc = coords[ vertices[ 0 ] ]
214
+ pl( [ bc, bc + ray_length * rd ], 'ray' )
215
+ return
216
+
217
+ # edge
218
+ if len( vertices ) == 2:
219
+ pl( [ coords[ vertices[ 0 ] ], coords[ vertices[ 1 ] ] ], 'edge' )
220
+ return
221
+
222
+ raise RuntimeError( "not an expected size for vertices" )
223
+
224
+ self.for_each_edge_td( edge_func )
225
+
226
+ def plot( self, fig = None, **kwargs ):
227
+ """
228
+ """
229
+ if fig is None:
230
+ import matplotlib.pyplot as plt
231
+ self.plot( plt )
232
+ plt.show()
233
+ return
234
+
235
+ self.plot_pyplot( fig, **kwargs )
236
+
237
+ # if self.true_dimensionality == 2:
238
+
239
+ # def disp_ray( ray_refs, vertex_index, rev ):
240
+ # print( "ray" )
241
+ # dir = self.ray_dir( ray_refs, vertex_index ) @ self.base.T
242
+ # b_c = coords[ vertex_index ]
243
+ # d_c = b_c + ray_length * dir / np.linalg.norm( dir )
244
+ # x = [ b_c[ 0 ], d_c[ 0 ] ]
245
+ # y = [ b_c[ 1 ], d_c[ 1 ] ]
246
+ # if rev:
247
+ # x.reverse()
248
+ # y.reverse()
249
+ # fig.plot( x, y, ray_linestyle or linestyle, color = ray_color or color )
250
+
251
+ # def on_face( edge_refs, vertex_indices, ray_refs ):
252
+ # print( "cut_refs" )
253
+
254
+ # if len( ray_refs ) >= 1:
255
+ # disp_ray( ray_refs[ 0 ], vertex_indices[ 0 ], 1 )
256
+
257
+ # if len( vertex_indices ) >= 2:
258
+ # x = [ coords[ n, 0 ] for n in vertex_indices ]
259
+ # y = [ coords[ n, 1 ] for n in vertex_indices ]
260
+ # if len( ray_refs ) == 0: # closed
261
+ # x.append( x[ 0 ] )
262
+ # y.append( y[ 0 ] )
263
+ # fig.plot( x, y, linestyle, color = color )
264
+
265
+ # if len( ray_refs ) >= 2:
266
+ # disp_ray( ray_refs[ 1 ], vertex_indices[ -1 ], 0 )
267
+
268
+ # self.for_each_face( on_face )
269
+ # return
270
+
271
+ # raise RuntimeError( f"TODO: plot for dim == { self.ndim }" )
272
+
@@ -0,0 +1,199 @@
1
+ from .PowerDiagram import PowerDiagram, rt_int
2
+ import numpy as np
3
+ import vfs
4
+
5
+ class ConvexFunction:
6
+ """
7
+ Function =
8
+ max(
9
+ -inf,
10
+ scalar_product( m_dirs[ 0 ], y ) - m_offs[ 0 ],
11
+ scalar_product( m_dirs[ 1 ], y ) - m_offs[ 1 ],
12
+ ...
13
+ )
14
+ for y such that
15
+ scalar_product( b_dirs[ 0 ], y ) - b_offs[ 0 ] <= 0,
16
+ scalar_product( b_dirs[ 1 ], y ) - b_offs[ 1 ] <= 0,
17
+ ...
18
+ """
19
+
20
+ def __init__( self, m_dirs, m_offs, b_dirs = None, b_offs = None, dtype = np.double ) -> None:
21
+ # m_...
22
+ self.m_dirs = np.asarray( m_dirs, dtype = dtype )
23
+ self.m_offs = np.asarray( m_offs, dtype = dtype )
24
+
25
+ assert( self.m_dirs.ndim == 2 )
26
+ assert( self.m_offs.ndim == 1 )
27
+ assert( self.m_offs.shape[ 0 ] == self.m_dirs.shape[ 0 ] )
28
+
29
+ # b_...
30
+ if b_dirs is None or not len( b_dirs ):
31
+ assert( b_offs is None or not len( b_offs ) )
32
+ b_dirs = np.zeros( [ 0, self.m_dirs.shape[ 1 ] ], dtype = dtype )
33
+ b_offs = np.zeros( [ 0 ], dtype = dtype )
34
+ self.b_dirs = np.asarray( b_dirs, dtype = dtype )
35
+ self.b_offs = np.asarray( b_offs, dtype = dtype )
36
+
37
+ assert( self.b_dirs.ndim == 2 )
38
+ assert( self.b_offs.ndim == 1 )
39
+ assert( self.b_offs.shape[ 0 ] == self.b_dirs.shape[ 0 ] )
40
+ assert( self.b_dirs.shape[ 1 ] == self.m_dirs.shape[ 1 ] )
41
+
42
+ # attributes by deduction
43
+ self.nb_dims = self.m_dirs.shape[ 1 ]
44
+
45
+ def make_approx_from_values_and_derivatives( sample_coords, f_val, f_der, b_dirs = None, b_offs = None ):
46
+ m_dirs = []
47
+ m_offs = []
48
+ for num_point in range( sample_coords.shape[ 1 ] ):
49
+ point = sample_coords[ :, num_point ]
50
+ val = f_val( point )
51
+ der = f_der( point )
52
+
53
+ m_dirs.append( der )
54
+ m_offs.append( np.dot( der, point ) - val )
55
+ return ConvexFunction( m_dirs, m_offs, b_dirs, b_offs )
56
+
57
+ def to_power_diagram( self ):
58
+ weights = np.sum( self.m_dirs * self.m_dirs, axis = 1 ) - 2 * self.m_offs
59
+ return PowerDiagram( self.m_dirs, weights, self.b_dirs, self.b_offs )
60
+
61
+ def write_vtk( self, filename, fit_boundary = 1.0 ):
62
+ pd = self.to_power_diagram()
63
+ pd.write_vtk( filename, fit_boundary )
64
+
65
+ def summary( self ):
66
+ from sympy import Symbol, tensorproduct
67
+
68
+ y = [ Symbol( f'y_{ d }' ) for d in range( self.nb_dims ) ]
69
+
70
+ repr_max = lambda i: str( sum( tensorproduct( self.m_dirs[ i, : ], y ) ) - self.m_offs[ i ] )
71
+ repr_bnd = lambda i: str( sum( tensorproduct( self.b_dirs[ i, : ], y ) ) <= self.b_offs[ i ] )
72
+
73
+ lst_max = [ repr_max( i ) for i in range( self.m_dirs.shape[ 0 ] ) ]
74
+ lst_bnd = [ repr_bnd( i ) for i in range( self.b_dirs.shape[ 0 ] ) ]
75
+ lst_max.sort()
76
+ lst_bnd.sort()
77
+
78
+ res = f"max({ ', '.join( lst_max ) })"
79
+ if len( lst_bnd ):
80
+ res += f" for { ', '.join( lst_bnd ) }"
81
+ return res
82
+
83
+ def legendre_transform( self ):
84
+ # call the legendre_transform func
85
+ func = vfs.function( 'legendre_transform', [ f'inc_file:sdot/legendre_transform/legendre_transform.h' ] )
86
+ new_m_dirs, new_m_offs, new_b_dirs, new_b_offs = func( self.m_dirs, self.m_offs, self.b_dirs, self.b_offs, rt_int( self.nb_dims ) )
87
+ new_m_dirs = np.reshape( new_m_dirs, [ -1, self.nb_dims ] )
88
+ new_b_dirs = np.reshape( new_b_dirs, [ -1, self.nb_dims ] )
89
+
90
+ # make a new ConvexFunction
91
+ return ConvexFunction( new_m_dirs, new_m_offs, new_b_dirs, new_b_offs )
92
+
93
+ def __repr__( self ):
94
+ return self.summary()
95
+
96
+ def is_inside_boundaries( self, x_k ):
97
+ if isinstance( x_k, ( int, float ) ):
98
+ x_k = [ x_k ]
99
+ x_k = np.asarray( x_k )
100
+
101
+ return self.b_dirs.shape[ 0 ] == 0 or ( self.b_dirs @ x_k - self.b_offs ).max() <= 0
102
+
103
+ def __call__( self, x_k ):
104
+ return self.val( x_k )
105
+
106
+ def val( self, x_k ):
107
+ if isinstance( x_k, ( int, float ) ):
108
+ x_k = [ x_k ]
109
+ x_k = np.asarray( x_k )
110
+
111
+ if not self.is_inside_boundaries( x_k ):
112
+ return float('inf')
113
+
114
+ return ( self.m_dirs @ x_k - self.m_offs ).max()
115
+
116
+ def grad( self, x_k ):
117
+ if isinstance( x_k, ( int, float ) ):
118
+ x_k = [ x_k ]
119
+ x_k = np.asarray( x_k )
120
+
121
+ if not self.is_inside_boundaries( x_k ):
122
+ return float('inf')
123
+
124
+ k = ( self.m_dirs @ x_k - self.m_offs ).argmax()
125
+ return self.m_dirs[ k ]
126
+
127
+ def __add__( self, u2 ):
128
+ u1 = self
129
+ if isinstance( u2, ConvexFunction ): # addition of two convex functions
130
+ if u1.nb_dims == u2.nb_dims:
131
+ nb_dims = u1.nb_dims
132
+ else:
133
+ print('Dimensions do not match.')
134
+
135
+ new_m_dirs = ( u1.m_dirs[ :, None, : ] + u2.m_dirs[ None, :, : ] ).reshape( ( -1, nb_dims ) )
136
+ new_m_offs = ( u1.m_dirs[ :, None ] + u2.m_dirs[ None, : ] ).flatten()
137
+ new_b_dirs = np.block( [ [ u1.b_dirs ], [ u2.b_dirs ] ] )
138
+ new_b_offs = np.concatenate( [ u1.b_offs, u2.b_offs ] )
139
+
140
+ return ConvexFunction( new_m_dirs, new_m_offs, new_b_dirs, new_b_offs )
141
+
142
+ elif isinstance( u2, ( int, float ) ): # addition of a polyhedral function and a scalar
143
+ return ConvexFunction( u1.m_dirs, u1.m_offs - u2, u1.b_dirs, u1.b_offs )
144
+
145
+ else:
146
+ raise NotImplementedError( "Can't add Polyhedral and {}".format( type( u2 ) ) )
147
+
148
+ def __radd__( self, scal ):
149
+ return self.__add__( scal )
150
+
151
+ def __sub__( self, scal ):
152
+ if isinstance( scal,( int, float ) ):
153
+ return ConvexFunction( self.m_dirs, self.m_offs + scal, self.b_dirs, self.b_offs )
154
+ else:
155
+ raise NotImplementedError( "Can't substract ConvexFunction from {}".format( type( scal ) ) )
156
+
157
+ def __mul__( self, scal ):
158
+ if isinstance( scal, ( int, float ) ):
159
+ assert( scal >= 0 )
160
+ return ConvexFunction( scal * self.m_dirs, scal * self.m_offs, self.b_dirs, self.b_offs )
161
+ else:
162
+ raise NotImplementedError( "Can't multiply ConvexFunction and {}".format( type( scal ) ) )
163
+
164
+ def __rmul__( self, scal ):
165
+ return self.__mul__( scal )
166
+
167
+ def __truediv__( self, scal ):
168
+ if isinstance( scal, ( int, float ) ):
169
+ assert( scal > 0 )
170
+ return ConvexFunction( self.m_dirs / scal, self.m_offs / scal, self.b_dirs, self.b_offs )
171
+ else:
172
+ raise NotImplementedError( "Can't divide {} by ConvexFunction.".format( type( scal ) ) )
173
+
174
+
175
+ def __or__(self,u2):
176
+ u1 = self
177
+ if isinstance( u2, ConvexFunction ): # maximum of two PHC functions
178
+ new_m_dirs = np.block( [ [ u1.m_dirs ], [ u2.m_dirs ] ] )
179
+ new_m_offs = np.concatenate( [ u1.m_offs, u2.m_offs ] )
180
+ new_b_dirs = np.block( [ [ u1.b_dirs ], [ u2.b_dirs ] ] )
181
+ new_b_offs = np.concatenate( [ u1.b_offs,u2.b_offs ] )
182
+ return ConvexFunction( new_m_dirs, new_m_offs, new_b_dirs, new_b_offs )
183
+
184
+ elif isinstance(u2,(int,float)): # addition of a polyhedral function and a scalar
185
+ return u1.__or__( ConvexFunction( np.zero( [ 1, u1.nb_dims ] ), [ -u2 ] ) )
186
+
187
+ else:
188
+ raise NotImplementedError( "Can't take the maximum of ConvexFunction and {}".format( type( u2 ) ) )
189
+
190
+ def __ror__( self, scal ):
191
+ return self.__or__( scal )
192
+
193
+
194
+ def __xor__( self, u2 ): # inf-convolution of two PHC functions
195
+ if isinstance( u2, ConvexFunction ):
196
+ return ( self.legendre_transform() + u2.legendre_transform() ).legendre_transform()
197
+
198
+ else:
199
+ raise NotImplementedError("Can't take the inf-convolution of ConvexFunction and {}".format(type(u2)))
@@ -0,0 +1,5 @@
1
+ class D2GTransportMap:
2
+ """
3
+ Discrete (diracs) to generic measure transport map (optimal or not)
4
+ """
5
+