Skip to content

Commit 1f5260d

Browse files
authored
Merge pull request #600 from bobmyhill/pycddlib3
enable pycddlib3
2 parents eade809 + 3c205d8 commit 1f5260d

File tree

7 files changed

+102
-84
lines changed

7 files changed

+102
-84
lines changed

.github/workflows/main.yml

+2-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on: [push, pull_request, workflow_dispatch]
66
jobs:
77
indent:
88
name: indent
9-
runs-on: [ubuntu-20.04]
9+
runs-on: [ubuntu-24.04]
1010

1111
strategy:
1212
matrix:
@@ -34,7 +34,7 @@ jobs:
3434
3535
linux:
3636
name: test
37-
runs-on: [ubuntu-20.04]
37+
runs-on: [ubuntu-24.04]
3838

3939
strategy:
4040
matrix:
@@ -48,14 +48,11 @@ jobs:
4848
python-version: ${{ matrix.python-versions }}
4949
- name: setup
5050
run: |
51-
# pycddlib requires libgmp3-dev
5251
sudo apt update && sudo apt install --yes \
5352
numdiff \
54-
libgmp3-dev \
5553
texlive \
5654
texlive-latex-extra
5755
python -m pip install --upgrade pip
58-
pip install pycddlib==2.1.7 # this is optional
5956
pip install autograd # this is optional
6057
python --version
6158
- name: test

burnman/classes/polytope.py

+74-67
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@
1212
from scipy.spatial import Delaunay
1313
from scipy.special import comb
1414
from copy import copy
15+
import cdd as cdd_float
1516

1617
from .material import cached_property
1718

1819
from ..utils.math import independent_row_indices
1920

21+
2022
try:
21-
cdd = importlib.import_module("cdd")
22-
except ImportError as err:
23-
print(
24-
f"Warning: {err}. "
25-
"For full functionality of BurnMan, please install pycddlib."
26-
)
23+
cdd_fraction = importlib.import_module("cdd.gmp")
24+
cdd_gmp_loaded = True
25+
except ImportError:
26+
cdd_fraction = importlib.import_module("cdd")
27+
cdd_gmp_loaded = False
2728

2829

2930
class SimplexGrid(object):
@@ -111,9 +112,7 @@ def n_points(self):
111112

112113
class MaterialPolytope(object):
113114
"""
114-
A class that can be instantiated to create pycddlib polytope objects.
115-
These objects can be interrogated to provide the vertices satisfying the
116-
input constraints.
115+
A class for creating and manipulating polytope objects using pycddlib.
117116
118117
This class is available as :class:`burnman.polytope.MaterialPolytope`.
119118
"""
@@ -122,7 +121,6 @@ def __init__(
122121
self,
123122
equalities,
124123
inequalities,
125-
number_type="fraction",
126124
return_fractions=False,
127125
independent_endmember_occupancies=None,
128126
):
@@ -132,31 +130,45 @@ def __init__(
132130
133131
:param equalities: A numpy array containing all the
134132
equalities defining the polytope. Each row should evaluate to 0.
135-
:type equalities: numpy.array (2D)
133+
:type equalities: numpy.array (2D) of floats or Fractions
136134
:param inequalities: A numpy array containing all the inequalities
137135
defining the polytope. Each row should evaluate to <= 0.
138-
:type inequalities: numpy.array (2D)
139-
:param number_type: Whether pycddlib should read the input arrays as
140-
fractions or floats. Valid options are 'fraction' or 'float'.
141-
:type number_type: str
142-
:param return_fractions: Choose whether the generated polytope object
143-
should return fractions or floats.
136+
:type inequalities: numpy.array (2D) of the same type as equalities
137+
:param return_fractions: Whether or not to return fractions.
144138
:type return_fractions: bool
145-
:param independent_endmember_occupancies: If specified, this array provides
146-
the independent endmember set against which the dependent endmembers
147-
are defined.
139+
:param independent_endmember_occupancies: If specified, this array
140+
provides the independent endmember set against which the
141+
dependent endmembers are defined.
148142
:type independent_endmember_occupancies: numpy.array (2D) or None
149143
"""
144+
if equalities.dtype != inequalities.dtype:
145+
raise Exception(
146+
f"The equalities and inequalities arrays should have the same type ({equalities.dtype} != {inequalities.dtype})."
147+
)
148+
150149
self.set_return_type(return_fractions)
151150
self.equality_matrix = equalities[:, 1:]
152151
self.equality_vector = -equalities[:, 0]
153152

154-
self.polytope_matrix = cdd.Matrix(
155-
equalities, linear=True, number_type=number_type
153+
if equalities.dtype == Fraction and cdd_gmp_loaded is True:
154+
self.number_type = Fraction
155+
self.cdd = cdd_fraction
156+
elif equalities.dtype == float or cdd_gmp_loaded is False:
157+
self.number_type = float
158+
self.cdd = cdd_float
159+
else:
160+
raise Exception(
161+
"equalities should be arrays of either floats or Fractions."
162+
)
163+
164+
self.polytope_matrix = self.cdd.matrix_from_array(equalities)
165+
self.polytope_matrix.lin_set = set(range(len(equalities)))
166+
self.polytope_matrix.rep_type = self.cdd.RepType.INEQUALITY
167+
168+
self.cdd.matrix_append_to(
169+
self.polytope_matrix, self.cdd.matrix_from_array(inequalities)
156170
)
157-
self.polytope_matrix.rep_type = cdd.RepType.INEQUALITY
158-
self.polytope_matrix.extend(inequalities, linear=False)
159-
self.polytope = cdd.Polyhedron(self.polytope_matrix)
171+
self.polytope = self.cdd.polyhedron_from_matrix(self.polytope_matrix)
160172

161173
if independent_endmember_occupancies is not None:
162174
self.independent_endmember_occupancies = independent_endmember_occupancies
@@ -182,14 +194,14 @@ def raw_vertices(self):
182194
Returns a list of the vertices of the polytope without any
183195
postprocessing. See also endmember_occupancies.
184196
"""
185-
return self.polytope.get_generators()[:]
197+
return self.cdd.copy_generators(self.polytope).array
186198

187199
@cached_property
188200
def limits(self):
189201
"""
190202
Return the limits of the polytope (the set of bounding inequalities).
191203
"""
192-
return np.array(self.polytope.get_inequalities(), dtype=float)
204+
return np.array(self.cdd.copy_inequalities(self.polytope).array, dtype=float)
193205

194206
@cached_property
195207
def n_endmembers(self):
@@ -205,27 +217,23 @@ def endmember_occupancies(self):
205217
Return the endmember occupancies
206218
(a processed list of all of the vertex locations).
207219
"""
208-
if self.return_fractions:
209-
if self.polytope.number_type == "fraction":
210-
v = np.array(
211-
[[Fraction(value) for value in v] for v in self.raw_vertices]
212-
)
213-
else:
214-
v = np.array(
215-
[
216-
[Rational(value).limit_denominator(1000000) for value in v]
217-
for v in self.raw_vertices
218-
]
219-
)
220+
if self.number_type == float and self.return_fractions:
221+
v = np.array(
222+
[
223+
[Fraction(value).limit_denominator(1000000) for value in v]
224+
for v in self.raw_vertices
225+
]
226+
)
227+
elif self.number_type == Fraction and not self.return_fractions:
228+
v = np.array(self.raw_vertices).astype(float)
220229
else:
221-
v = np.array([[float(value) for value in v] for v in self.raw_vertices])
230+
v = np.array(self.raw_vertices)
222231

223232
if len(v.shape) == 1:
224233
raise ValueError(
225234
"The combined equality and positivity "
226235
"constraints result in a null polytope."
227236
)
228-
229237
return v[:, 1:] / v[:, 0, np.newaxis]
230238

231239
@cached_property
@@ -281,37 +289,41 @@ def independent_endmember_polytope(self):
281289
"""
282290
arr = self.endmembers_as_independent_endmember_amounts
283291
arr = np.hstack((np.ones((len(arr), 1)), arr[:, :-1]))
284-
M = cdd.Matrix(arr, number_type="fraction")
285-
M.rep_type = cdd.RepType.GENERATOR
286-
return cdd.Polyhedron(M)
292+
arr = [[Fraction(value).limit_denominator(1000000) for value in v] for v in arr]
293+
M = cdd_fraction.matrix_from_array(arr)
294+
M.rep_type = cdd_fraction.RepType.GENERATOR
295+
return cdd_fraction.polyhedron_from_matrix(M)
287296

288297
@cached_property
289298
def independent_endmember_limits(self):
290299
"""
291300
Gets the limits of the polytope as a function of the independent
292301
endmembers.
293302
"""
294-
return np.array(
295-
self.independent_endmember_polytope.get_inequalities(), dtype=float
296-
)
303+
ind_poly = self.independent_endmember_polytope
304+
inequalities = cdd_fraction.copy_inequalities(ind_poly).array
305+
return np.array(inequalities, dtype=float)
297306

298307
def subpolytope_from_independent_endmember_limits(self, limits):
299308
"""
300309
Returns a smaller polytope by applying additional limits to the amounts
301310
of the independent endmembers.
302311
"""
303-
modified_limits = self.independent_endmember_polytope.get_inequalities().copy()
304-
modified_limits.extend(limits, linear=False)
305-
return cdd.Polyhedron(modified_limits)
312+
ind_poly = self.independent_endmember_polytope
313+
modified_limits = cdd_fraction.copy_inequalities(ind_poly)
314+
cdd_fraction.matrix_append_to(
315+
modified_limits, cdd_fraction.matrix_from_array(limits)
316+
)
317+
return cdd_fraction.polyhedron_from_matrix(modified_limits)
306318

307319
def subpolytope_from_site_occupancy_limits(self, limits):
308320
"""
309321
Returns a smaller polytope by applying additional limits to the
310322
individual site occupancies.
311323
"""
312-
modified_limits = self.polytope_matrix.copy()
313-
modified_limits.extend(limits, linear=False)
314-
return cdd.Polyhedron(modified_limits)
324+
modified_limits = copy(self.polytope_matrix)
325+
self.cdd.matrix_append_to(modified_limits, self.cdd.matrix_from_array(limits))
326+
return self.cdd.polyhedron_from_matrix(modified_limits)
315327

316328
def grid(
317329
self,
@@ -325,15 +337,16 @@ def grid(
325337
326338
:param points_per_edge: Number of points per edge of the polytope.
327339
:type points_per_edge: int
328-
:param unique_sorted: The gridding is done by splitting the polytope into
329-
a set of simplices. This means that points will be duplicated along
330-
vertices, faces etc. If unique_sorted is True, this function
340+
:param unique_sorted: The gridding is done by splitting the polytope
341+
into a set of simplices. This means that points will be duplicated
342+
along vertices, faces etc. If unique_sorted is True, this function
331343
will sort and make the points unique. This is an expensive
332344
operation for large polytopes, and may not always be necessary.
333345
:type unique_sorted: bool
334346
:param grid_type: Whether to grid the polytope in terms of
335347
independent endmember proportions or site occupancies.
336-
Choices are 'independent endmember proportions' or 'site occupancies'
348+
Choices are 'independent endmember proportions' or
349+
'site occupancies'
337350
:type grid_type: str
338351
:param limits: Additional inequalities restricting the
339352
gridded area of the polytope.
@@ -361,11 +374,8 @@ def grid(
361374
)
362375
else:
363376
if grid_type == "independent endmember proportions":
364-
ppns = np.array(
365-
self.subpolytope_from_independent_endmember_limits(
366-
limits
367-
).get_generators()[:]
368-
)[:, 1:]
377+
plims = self.subpolytope_from_site_occupancy_limits(limits)
378+
ppns = np.array(self.cdd.copy_generators(plims).array)[:, 1:]
369379
last_ppn = np.array([1.0 - sum(p) for p in ppns]).reshape(
370380
(len(ppns), 1)
371381
)
@@ -377,11 +387,8 @@ def grid(
377387
)
378388

379389
elif grid_type == "site occupancies":
380-
occ = np.array(
381-
self.subpolytope_from_site_occupancy_limits(
382-
limits
383-
).get_generators()[:]
384-
)[:, 1:]
390+
plims = self.subpolytope_from_site_occupancy_limits(limits)
391+
occ = np.array(self.cdd.copy_generators(plims).array)[:, 1:]
385392
f_occ = occ / (points_per_edge - 1)
386393

387394
ind = self.independent_endmember_occupancies

burnman/tools/chemistry.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -276,21 +276,28 @@ def reactions_from_stoichiometric_matrix(stoichiometric_matrix):
276276
"""
277277
n_components = stoichiometric_matrix.shape[1]
278278

279-
equalities = np.concatenate(([np.zeros(n_components)], stoichiometric_matrix)).T
279+
equalities = np.concatenate(
280+
([np.zeros(n_components)], np.array(stoichiometric_matrix).astype(float))
281+
).T
280282

281283
polys = [
282-
MaterialPolytope(equalities, np.diag(v))
284+
MaterialPolytope(equalities, np.diag(v).astype(float))
283285
for v in itertools.product(*[[-1, 1]] * len(equalities[0]))
284286
]
285287
reactions = []
286288
for p in polys:
287-
v = np.array([[value for value in v] for v in p.raw_vertices])
289+
v = np.array(
290+
[
291+
[Rational(value).limit_denominator(1000000) for value in v]
292+
for v in p.raw_vertices
293+
],
294+
dtype=float,
295+
)
288296

289297
if v is not []:
290298
reactions.extend(v)
291299

292-
reactions = np.unique(np.array(reactions, dtype=float), axis=0)
293-
300+
reactions = np.unique(np.array(reactions), axis=0)
294301
reactions = np.array(
295302
[[Rational(value).limit_denominator(1000000) for value in v] for v in reactions]
296303
)

burnman/tools/polytope.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,7 @@ def composite_polytope_at_constrained_composition(
164164
eoccs = block_diag(*eoccs)
165165
inequalities = np.concatenate((np.zeros((len(eoccs), 1)), eoccs), axis=1)
166166

167-
return MaterialPolytope(
168-
equalities, inequalities, number_type="float", return_fractions=return_fractions
169-
)
167+
return MaterialPolytope(equalities, inequalities, return_fractions=return_fractions)
170168

171169

172170
def simplify_composite_with_composition(composite, composition):

examples/example_polytopetools.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"""
3636
from __future__ import absolute_import
3737
import numpy as np
38+
from sympy import Rational
3839

3940
import burnman
4041
from burnman.minerals import SLB_2011
@@ -197,14 +198,21 @@
197198
)
198199
print([formula_to_string(f) for f in assemblage.endmember_formulae])
199200

201+
# The conversions from Fraction to Rational below
202+
# are solely for pretty-printing purposes.
200203
print(
201204
"Which led to the following extreme vertices satifying "
202205
"the bulk compositional constraints:"
203206
)
204-
print(old_polytope.endmember_occupancies)
207+
208+
print(
209+
np.array([[Rational(v) for v in i] for i in old_polytope.endmember_occupancies])
210+
)
205211

206212
print("\nThe new assemblage has fewer endmembers, with compositions:")
207213
print([formula_to_string(f) for f in new_assemblage.endmember_formulae])
208214

209215
print("The same vertices now look like this:")
210-
print(new_polytope.endmember_occupancies)
216+
print(
217+
np.array([[Rational(v) for v in i] for i in new_polytope.endmember_occupancies])
218+
)

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ sympy = "^1.12"
2525
cvxpy = "^1.3"
2626
matplotlib = "^3.7"
2727
numba = "^0.59"
28+
pycddlib-standalone = "^3.0"
2829

2930
[tool.poetry.dev-dependencies]
3031
ipython = "^8.5"

test.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ $PYTHON -m pipdeptree -p burnman -d 1 2> /dev/null
2929
echo ""
3030

3131
# Quietly install optional modules after burnman
32-
echo "Installing optional cvxpy, pycddlib, autograd and jupyter modules ..."
33-
$PYTHON -m pip install -q cvxpy pycddlib==2.1.7 autograd jupyter
32+
echo "Installing optional autograd and jupyter modules ..."
33+
$PYTHON -m pip install -q autograd jupyter
3434
echo ""
3535

3636
function testit {

0 commit comments

Comments
 (0)