lattice.py 22.7 KB
Newer Older
MischaD's avatar
MischaD committed
1
import numpy as np
MischaD's avatar
MischaD committed
2
import sympy as sp
MischaD's avatar
MischaD committed
3
import random
MischaD's avatar
MischaD committed
4
from sympy import oo
Mischa Dombrowski's avatar
Mischa Dombrowski committed
5
from .utils.mylog import logger
MischaD's avatar
MischaD committed
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from .exceptions import (
    OrderNotImplementedException,
    ImpossibleVelocityShellException,
    ReducibleShellException,
    NoSolutionException,
    InfiniteSolutionsException,
    UninitializedAttributeException,
)
from .utils.utils import (
    analyse_tensor_dimension,
    get_group,
    get_subshells,
    lattice_sum,
    approximate_ratio,
    timer,
MischaD's avatar
MischaD committed
21
    ps_installed,
MischaD's avatar
MischaD committed
22
23
24
    get_random_vector,
    velocities_for_shell,
)
25
from .shell import Shell
MischaD's avatar
MischaD committed
26
27


MischaD's avatar
MischaD committed
28
class Lattice:
MischaD's avatar
MischaD committed
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    BY_NAME = {
        "D2Q9": {"dimension": 2, "order": 4, "shell_list": [1, 2, 4], "seed": 44},
        "D2V17": {
            "dimension": 2,
            "order": 6,
            "shell_list": [1, 2, 4, 8, 9],
            "seed": 44,
        },
        "D2V37": {
            "dimension": 2,
            "order": 8,
            "shell_list": [1, 2, 4, 5, 8, 9, 10, 16],
            "seed": 44,
        },
        "D3Q19": {"dimension": 3, "order": 4, "shell_list": [1, 2, 4], "seed": 44},
        "D3Q15": {"dimension": 3, "order": 4, "shell_list": [1, 3, 4], "seed": 44},
        "D3Q41-ZOT": {
            "dimension": 3,
            "order": 6,
            "shell_list": [1, 2, 3, 9, 16, 27],
            "boundary": "sup",
            "unwanted_subshells": ["221", "511"],
            "seed": 44,
        },
    }

    def __init__(
        self,
        dimension=2,
        order=4,
        shell_list=[1, 2, 4],
        seed=None,
        boundary=None,
        unwanted_subshells=[],
mischa's avatar
mischa committed
63
        eq_order=None,
MischaD's avatar
MischaD committed
64
65
        svd_tolerance=1e-8,
    ):
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
        """Create an instance of the Lattice class without calculating the weights.
        Default values are chosen to lead to the standard D2Q9 lattice.
        Not all input values are valid. :func:`Lattice.init_by_order`

        Args:
            name: Initialize Lattice with the correct values for some known velocities. Possible names are
                D2Q9, D2V17, D2V37, D3Q17,
            dimension (int): Dimension of the resulting lattice. Default 2.
            order (int): Order of tensor isotropy. Only odd orders are
            shell_list (list): List of the Squared lengths of the velocities.
            unwanted_subshells (list) : List of strings that correspond to the type of a subshell that isn't supposed to
                to be part of lattice. Some shells are ambiguous and in the default case all possiblities are taken,
                but some lattice, like the D3Q41-ZOT need certain subshells to be remove. E.g. 2 Dimensions and
                shell 25 is added. Bother type = "50" and type = "43" could be used. Add one of them to the list of
                unwanted subshells to remove them.
mischa's avatar
mischa committed
81
82
            eq_order (int): Explicitly specify the order of accuracy for the equilibrium distribution function.
                Only relevant if AssignmentCollections are used. For 2D resonable defaults are used.
83
84
            seed (float): Seed for the random number generator.
            svd_tolerance (float): tolerance to set the singular values to zero after svd.
MischaD's avatar
MischaD committed
85

MischaD's avatar
MischaD committed
86
        """
MischaD's avatar
MischaD committed
87
        self._dimension = dimension
MischaD's avatar
MischaD committed
88
89
90
        self._order = order
        self._shell_list = shell_list
        self._seed = seed
mischa's avatar
mischa committed
91
        self._eq_order = eq_order
MischaD's avatar
MischaD committed
92
        self._svd_tolerance = svd_tolerance
MischaD's avatar
MischaD committed
93
        random.seed(self._seed)
MischaD's avatar
MischaD committed
94

MischaD's avatar
MischaD committed
95
        # Preperation
MischaD's avatar
MischaD committed
96
        self._tensor_space_dimension = 0
MischaD's avatar
MischaD committed
97
        self._all_velocity_vectors = []
MischaD's avatar
MischaD committed
98
        self._possible_tensors = []
MischaD's avatar
MischaD committed
99
        self._tensor_dimensions = []
MischaD's avatar
MischaD committed
100
        self._shells = 0
101
        self._unwanted_subshells = unwanted_subshells
MischaD's avatar
MischaD committed
102

MischaD's avatar
MischaD committed
103
104
105
106
        # LES
        self._lhs = []
        self._rhs = []

MischaD's avatar
MischaD committed
107
108
        # SVD
        self._singular_values = []
MischaD's avatar
MischaD committed
109
        self._solution = []
MischaD's avatar
MischaD committed
110

111
        # Polynomials
MischaD's avatar
MischaD committed
112
        self._coefficients = []
MischaD's avatar
MischaD committed
113
114
        self._weight_polynomials = []
        self._interval = None
115
        self._boundary = boundary if boundary == "sup" else "inf"
MischaD's avatar
MischaD committed
116

117
118
119
        # Final values
        self._discrete_velocities = []
        self._weights = []
mischa's avatar
mischa committed
120
        self._q = None
121
122
        self._c_s_sq = None

MischaD's avatar
MischaD committed
123
124
    def __str__(self):
        string = "D{} - Order: {} - Shells: {}".format(
MischaD's avatar
MischaD committed
125
            self._dimension, self._order, str(self._shell_list)
MischaD's avatar
MischaD committed
126
        )
MischaD's avatar
MischaD committed
127
128
129
        if self._boundary:
            string += " - Boundary: {}".format(self._boundary)
        if self._unwanted_subshells:
MischaD's avatar
MischaD committed
130
131
132
            string += " - Unwanted Subshells: {}".format(
                "".join([s + ", " for s in self._unwanted_subshells])
            )
MischaD's avatar
MischaD committed
133
134
135
        return string

    @classmethod
MischaD's avatar
MischaD committed
136
    def from_name(cls, name):
137
138
139
140
141
        """Initiate the Lattice with the correct values corresponding to a know Lattice in literature.
        A complete list can be seen in Lattice.BY_NAME.keys()"""
        kwargs = cls.BY_NAME.get(name)
        if not kwargs:
            raise ValueError("No Lattice with this name is known or implemented")
MischaD's avatar
MischaD committed
142
        return cls(**kwargs)
143
144

    @classmethod
MischaD's avatar
MischaD committed
145
    def from_order(cls, dimension, order):
MischaD's avatar
MischaD committed
146
147
148
149
150
        """
        :param dimension:
        :param order:
        :return:
        """
MischaD's avatar
MischaD committed
151
152
153
154
155
156
157
158
        if order % 2 != 0:
            order -= 1

        for key, value in cls.BY_NAME.items():
            if value.get("dimension") == dimension and value.get("order") == order:
                return cls(**cls.BY_NAME.get(key))

        raise ValueError(
MischaD's avatar
MischaD committed
159
            "No Lattice known with order {} and dimension {}.".format(order, dimension)
MischaD's avatar
MischaD committed
160
        )
MischaD's avatar
MischaD committed
161
        return cls(dimension=dimension, order=order, shell_list=shell_list, seed=seed)
MischaD's avatar
MischaD committed
162

MischaD's avatar
MischaD committed
163
164
165
166
    @property
    def tensor_space_dimension(self):
        return np.array([len(x) for x in self._possible_tensors]).sum()

MischaD's avatar
MischaD committed
167
168
    @property
    def shells(self):
169
        return self._shells
MischaD's avatar
MischaD committed
170

mischa's avatar
mischa committed
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
    @property
    def q(self):
        if self._q:
            return self._q
        self._q = len(self.velocity_set()[1])
        return self._q

    @property
    def c_s_sq(self):
        if self._c_s_sq:
            return self._c_s_sq
        return self.velocity_set()[0]

    @property
    def weights(self):
        if self._weights:
            return self._weights
        return self.calculate_weights()

MischaD's avatar
MischaD committed
190
191
    @property
    def velocities(self):
MischaD's avatar
MischaD committed
192
193
        """Get a list of all types of velocities. Velocities with zero weight are included
        For D2O9 this equals [00, 10, 11, 20]"""
194
195
        if not self._discrete_velocities and self._shells:
            self._discrete_velocities = [shell.type for shell in self._shells]
196
        return self._discrete_velocities
MischaD's avatar
MischaD committed
197

198
    @property
mischa's avatar
mischa committed
199
200
201
202
203
204
    def eq_order(self):
        """Calculate the eq order based on the amount of velocities given and dimension.
        According to Philippi et. al, 17 and 37 velocities are the minimum amount respectively
        to use for third and fourth order accurate models. """
        if self._eq_order:
            return self._eq_order
205

mischa's avatar
mischa committed
206
207
208
209
210
211
212
213
214
215
216
        if self._dimension == 2:
            if self.q >= 37:
                self._eq_order = 4
            elif self.q >= 17:
                self._eq_order = 3
            else:
                self._eq_order = 2
        if self._dimension == 3:
            # Should be done explicitly for three dimensions using the constructor
            self._eq_order = 2
        return self._eq_order
MischaD's avatar
MischaD committed
217

MischaD's avatar
MischaD committed
218
219
    @property
    @ps_installed
mischa's avatar
mischa committed
220
    def assignment_collection(self, ps):
MischaD's avatar
MischaD committed
221
222
        if self._dimension == 3:
            return self._assignment_collection3d()
mischa's avatar
mischa committed
223
224
225
226
227
        c_s_sq, weights, velocities = self.velocity_set()
        Q = self.q
        c_ix = velocities[0]
        c_iy = velocities[1]
        src, dst = ps.fields("src({Q}), dst({Q}): double[2D]".format(Q=Q))
MischaD's avatar
MischaD committed
228
229
230
231
232
233
234
235
236
237
238
        density, u_x, u_y, omega = sp.symbols("density, u_x, u_y, omega")

        density_formula = sum([src[0, 0](i) for i in range(Q)])
        vel_x_formula = (sum([src[0, 0](i) * c_ix[i] for i in range(Q)])) / density
        vel_y_formula = (sum([src[0, 0](i) * c_iy[i] for i in range(Q)])) / density

        symbolic_description = [ps.Assignment(density, density_formula),
                                ps.Assignment(u_x, vel_x_formula),
                                ps.Assignment(u_y, vel_y_formula), ]

        for i in range(Q):
mischa's avatar
mischa committed
239
            feq_formula = weights[i] * density[0, 0] * (
MischaD's avatar
MischaD committed
240
241
242
243
244
                    1
                    + (u_x * c_ix[i] + u_y * c_iy[i]) / c_s_sq
                    + (u_x * c_ix[i] + u_y * c_iy[i]) ** 2 / (2 * c_s_sq ** 2)
                    - (u_x ** 2 + u_y ** 2) / (2 * c_s_sq)
            )
mischa's avatar
mischa committed
245
246
247
248
            if self.eq_order >= 3:
                feq_formula += weights[i] * density[0, 0] * (
                    -1 / (2 * c_s_sq**2) * (u_x * c_ix[i] + u_y * c_iy[i]) * (u_x ** 2 + u_y ** 2)
                    + 4 / 3 * (1 / (2 * c_s_sq)) ** 3 * (u_x * c_ix[i] + u_y * c_iy[i]) ** 3
MischaD's avatar
MischaD committed
249
                )
mischa's avatar
mischa committed
250
251
            if self.eq_order >= 4:
                feq_formula += weights[i] * density[0, 0] * \
MischaD's avatar
MischaD committed
252
253
254
255
256
257
258
259
260
                               (1 / (6 * c_s_sq ** 2) * (
                                       (u_x * c_ix[i] + u_y * c_iy[i]) ** 4 / (4 * c_s_sq ** 2) - (
                                           3 * (u_x ** 2 + u_y ** 2) *
                                           (u_x * c_ix[i] + u_y * c_iy[i]) ** 2) / (2 * c_s_sq) + 3 * (
                                                   u_x ** 4 + u_y ** 4)))

            symbolic_description.append(
                ps.Assignment(dst[-1 * c_iy[i], c_ix[i]](i), omega * feq_formula + (1 - omega) * src[0, 0](i))
            )
mischa's avatar
mischa committed
261
            print("okakychanp")
MischaD's avatar
MischaD committed
262
263
        return ps.AssignmentCollection(symbolic_description)

mischa's avatar
mischa committed
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
    @property
    @ps_installed
    def _assignment_collection3d(self, ps):
        c_s_sq, weights, velocities = self.velocity_set()
        Q = self.q
        c_ix = velocities[0]
        c_iy = velocities[1]
        c_iz = velocities[2]
        src, dst = ps.fields("src({Q}), dst({Q}): double[2D]".format(Q=Q))
        density, u_x, u_y, u_z, omega = sp.symbols("density, u_x, u_y, u_z, omega")

        density_formula = sum([src[0, 0](i) for i in range(Q)])
        vel_x_formula = (sum([src[0, 0](i) * c_ix[i] for i in range(Q)])) / density
        vel_y_formula = (sum([src[0, 0](i) * c_iy[i] for i in range(Q)])) / density
        vel_z_formula = (sum([src[0, 0](i) * c_iz[i] for i in range(Q)])) / density

        symbolic_description = [ps.Assignment(density, density_formula),
                                ps.Assignment(u_x, vel_x_formula),
                                ps.Assignment(u_y, vel_y_formula),
                                ps.Assignment(u_z, vel_z_formula),]

        for i in range(Q):
            feq_formula = weights[i] * density[0, 0] * (
                    1
                    + (u_x * c_ix[i] + u_y * c_iy[i] + u_z * c_iz[i]) / c_s_sq
                    + (u_x * c_ix[i] + u_y * c_iy[i] + u_z * c_iz[i]) ** 2 / (2 * c_s_sq ** 2)
                    - (u_x ** 2 + u_y ** 2 + u_z ** 2) / (2 * c_s_sq)
            )
            if self.eq_order >= 3:
                feq_formula += weights[i] * density[0, 0] * (
                        -1 / (2 * c_s_sq ** 2) * (u_x * c_ix[i] + u_y * c_iy[i] + u_z * c_iz[i]) * (u_x ** 2 + u_y ** 2 + u_z ** 2)
                        + 4 / 3 * (1 / (2 * c_s_sq)) ** 3 * (u_x * c_ix[i] + u_y * c_iy[i] + u_z * c_iz[i]) ** 3
                )
            if self.eq_order >= 4:
                feq_formula += weights[i] * density[0, 0] * \
                               (1 / (6 * c_s_sq ** 2) * (
                                       (u_x * c_ix[i] + u_y * c_iy[i] + u_z * c_iz[i]) ** 4 / (4 * c_s_sq ** 2) - (
                                           3 * (u_x ** 2 + u_y ** 2 + u_z ** 2) *
                                           (u_x * c_ix[i] + u_y * c_iy[i] + u_z * c_iz[i]) ** 2) / (2 * c_s_sq) + 3 * (
                                                   u_x ** 4 + u_y ** 4 + u_z ** 4)))

        for i in range(Q):
            symbolic_description.append(
                ps.Assignment(dst[-1 * c_iy[i], c_ix[i]](i), omega * feq_formula + (1 - omega) * src[0, 0](i))
            )
        return ps.AssignmentCollection(symbolic_description)
MischaD's avatar
MischaD committed
310

MischaD's avatar
MischaD committed
311
312
313
314
315
    def shell_from_type(self, type):
        for shell in self._shells:
            if shell.type == type:
                return shell

MischaD's avatar
MischaD committed
316
    def _calculate_velocity_vectors(self):
MischaD's avatar
MischaD committed
317
        velocity_vectors = []
MischaD's avatar
MischaD committed
318

MischaD's avatar
MischaD committed
319
        group = get_group(self._dimension)
MischaD's avatar
MischaD committed
320
        for shell in range(len(self._shell_list)):
MischaD's avatar
MischaD committed
321
322
323
            velocities = velocities_for_shell(
                dimension=self._dimension, shell=self._shell_list[shell]
            )
MischaD's avatar
MischaD committed
324
            if len(velocities) == 0:
MischaD's avatar
MischaD committed
325
326
327
                raise ImpossibleVelocityShellException(
                    "No velocity sets found for velocity shell {}.".format(shell)
                )
MischaD's avatar
MischaD committed
328

329
            # If e.g. dimension = 2, then shell 25 in ambiguous. Both shell (3,4) and (5,0) are added
MischaD's avatar
MischaD committed
330
331
            subshells = get_subshells(velocities, group)
            if len(subshells) > 1:
MischaD's avatar
MischaD committed
332
                # 3 4, 0 5 can only be given together
333
                velocities = []
MischaD's avatar
MischaD committed
334
                for i_subs, subshell in enumerate(subshells):
335
                    # get type for subshell. Example shell: all vectors of type [0, -1, 0] --> 100
MischaD's avatar
MischaD committed
336
337
338
                    type = "".join(
                        [str(int(x)) for x in sorted(abs(subshell[0]), reverse=True)]
                    )
339
340
                    if type not in self._unwanted_subshells:
                        velocities.append(subshell)
MischaD's avatar
MischaD committed
341
342
            else:
                velocities = [velocities]
MischaD's avatar
MischaD committed
343
            velocity_vectors.extend(velocities)
MischaD's avatar
MischaD committed
344

MischaD's avatar
MischaD committed
345
        self._shells = [Shell([np.array([0] * self._dimension)])]
346
347
        for final_velocities in velocity_vectors:
            self._shells.append(Shell(final_velocities))
MischaD's avatar
MischaD committed
348
349
        self._all_velocity_vectors = velocity_vectors
        return velocity_vectors
MischaD's avatar
MischaD committed
350

MischaD's avatar
MischaD committed
351
    def _fill_lhs(self):
MischaD's avatar
MischaD committed
352
353
354
355
        lhs = []
        for i, rank in enumerate(np.arange(2, self._order + 2, 2)):
            tensor_space_dimension = self._tensor_dimensions[i]
            for j in range(tensor_space_dimension):
MischaD's avatar
MischaD committed
356
                vector = get_random_vector(self._dimension)
MischaD's avatar
MischaD committed
357
358
                # vector = 2 * np.random.rand(self._dimension) - 1
                # vector = vector / np.linalg.norm(vector)
MischaD's avatar
MischaD committed
359
360

                rows = []
MischaD's avatar
MischaD committed
361
                for shell_velocities in self._all_velocity_vectors:
MischaD's avatar
MischaD committed
362
363
364
                    shell_sum = lattice_sum(vector, shell_velocities, rank)
                    rows.append(shell_sum)
                lhs.append(rows)
MischaD's avatar
MischaD committed
365
        self._lhs = np.array(lhs)
MischaD's avatar
MischaD committed
366
        return np.array(lhs)
MischaD's avatar
MischaD committed
367

MischaD's avatar
MischaD committed
368
    def _fill_rhs(self):
MischaD's avatar
MischaD committed
369
370
371
372
373
374
375
376
377
378
379
380
        rhs = []
        cols = self._order // 2
        for k, rank in enumerate(np.arange(2, self._order + 2, 2)):
            for j in range(self._tensor_dimensions[k]):
                rows = []
                for i in range(cols):
                    c_s_power = 2 * i + 2
                    rows.append(1 if c_s_power == rank else 0)
                rhs.append(rows)
        self._rhs = np.array(rhs)
        return np.array(rhs)

MischaD's avatar
MischaD committed
381
    def _svd(self):
MischaD's avatar
MischaD committed
382
383
384
        U, s, V = np.linalg.svd(self._lhs)
        rows = self._lhs.shape[0]
        cols = self._lhs.shape[1]
MischaD's avatar
MischaD committed
385
386
387
388

        self._singular_values = len(s)
        s = np.array([sv if sv > self._svd_tolerance else 0 for sv in s])
        rank = np.count_nonzero(s)
MischaD's avatar
MischaD committed
389
        rhs = np.dot(np.transpose(U), self._rhs)
MischaD's avatar
MischaD committed
390
391
392
393
        if rank < rows:
            overlap = rows - rank
            if np.linalg.norm(rhs[-overlap:]) > self._svd_tolerance:
                # NO Solution
MischaD's avatar
MischaD committed
394
395
396
                raise NoSolutionException(
                    "Add shells or lower the order"
                )  # change shells ?
MischaD's avatar
MischaD committed
397
398
            rows = rank
            s.resize(rows)
MischaD's avatar
MischaD committed
399
            rhs.resize((rows, rhs.shape[1]), refcheck=False)
MischaD's avatar
MischaD committed
400
401
402
403
404
405
406
407
408
409
410
411
            self._singular_values = len(s)

        reduced_rhs = np.zeros((rows, rhs.shape[1]))
        for i, SingularValue in enumerate(s):
            for j in range(rhs.shape[1]):
                reduced_rhs[i, j] = rhs[i, j] / SingularValue

        reduced_rhs = np.zeros((rows, rhs.shape[1]))
        for i, singular_value in enumerate(s):
            reduced_rhs[i, :] = rhs[i, :] / singular_value

        if cols - rows > 0:
MischaD's avatar
MischaD committed
412
413
414
415
            raise InfiniteSolutionsException(
                "Reduce order, remove shells, or use continue.py in "
                "https://github.com/BDuenweg/Lattice-Boltzmann-weights"
            )
MischaD's avatar
MischaD committed
416
417
        self._solution = np.dot(np.transpose(V), reduced_rhs)
        return self._solution
MischaD's avatar
MischaD committed
418

MischaD's avatar
MischaD committed
419
    def _calculate_coefficients(self):
MischaD's avatar
MischaD committed
420
421
422
423
        """Use _all_velocity_vectors and _solution to calculate the coefficients of the
        weigth polynomials. Writes them into _coefficients
        """

MischaD's avatar
MischaD committed
424
425
426
427
428
        solution_columns = self._order // 2
        coefficients = np.zeros((1 + solution_columns))
        coefficients[0] = 1
        for j in range(solution_columns):
            tmp = 0
MischaD's avatar
MischaD committed
429
430
431
            for i in range(len(self._all_velocity_vectors)):
                tmp -= self._solution[i, j] * len(self._all_velocity_vectors[i])
            coefficients[j + 1] = tmp
MischaD's avatar
MischaD committed
432

MischaD's avatar
MischaD committed
433
        coeffs = np.zeros((self._solution.shape[0] + 1, self._solution.shape[1] + 1))
MischaD's avatar
Bugfix    
MischaD committed
434
        coeffs[0, :] = coefficients[-1::-1]
MischaD's avatar
MischaD committed
435
436
437
438
        coeffs[1:, :-1] = self._solution[:, -1::-1]
        self._coefficients = coeffs
        return self._coefficients

MischaD's avatar
MischaD committed
439
    def _calculate_valid_interval(self):
MischaD's avatar
MischaD committed
440
        if not self._weight_polynomials:
MischaD's avatar
MischaD committed
441
442
443
            raise UninitializedAttributeException(
                "Weight Polynomials not give. Calculate them first!"
            )
MischaD's avatar
MischaD committed
444
445
446
447
448
449
450
451
452

        interval = sp.Interval(-oo, oo)
        for equation in self._weight_polynomials:
            roots = equation.real_roots()
            if not roots:
                continue
            roots = [roots[0] - 1] + roots + [roots[-1] + 1]
            cur_interval = sp.EmptySet()
            for i in range(len(roots) - 1):
MischaD's avatar
MischaD committed
453
                mesh_point = (roots[i + 1] + roots[i]) / 2
MischaD's avatar
MischaD committed
454
455
456
457
                if equation.eval(mesh_point) < 0:
                    continue
                # positive weights
                if i == 0:
MischaD's avatar
MischaD committed
458
459
460
                    cur_interval = sp.Union(
                        cur_interval, sp.Interval(-oo, roots[i + 1])
                    )
MischaD's avatar
MischaD committed
461
462
463
                elif i == (len(roots) - 2):
                    cur_interval = sp.Union(cur_interval, sp.Interval(roots[i], oo))
                else:
MischaD's avatar
MischaD committed
464
465
466
                    cur_interval = sp.Union(
                        cur_interval, sp.Interval(roots[i], roots[i + 1])
                    )
MischaD's avatar
MischaD committed
467
468
469
470
            interval = interval.intersect(cur_interval)
        self._interval = sp.Complement(interval, sp.FiniteSet(0))
        return self._interval

MischaD's avatar
MischaD committed
471
472
473
474
475
476
477
    def solution_expected(self):
        """Evaluate if the order, dimension and amount of shells entered are expected to retrieve a solution.
        This is in no way a guarantee on the existence of a solution or whether or not the program
        will finish successful, but it can be used as a quick hint on the existence of a solution.

        Changes the state of self._solution and self._velocity vectors for future calculations."""

MischaD's avatar
MischaD committed
478
479
480
481
482
483
484
485
486
        if self._order % 2 == 1:
            self._order -= 1
            logger.warning("Only odd order valid. Decrease by one")

        for k in np.arange(2, self._order + 2, 2):
            possible_tensors = analyse_tensor_dimension(k)
            self._possible_tensors.append(possible_tensors)
        self._tensor_dimensions = [len(x) for x in self._possible_tensors]

MischaD's avatar
MischaD committed
487
488
489
        self._calculate_velocity_vectors()
        self._fill_lhs()
        self._fill_rhs()
MischaD's avatar
MischaD committed
490

MischaD's avatar
MischaD committed
491
        self._svd()
MischaD's avatar
MischaD committed
492

MischaD's avatar
MischaD committed
493
        return self._solution is not None
MischaD's avatar
Bugfix    
MischaD committed
494

MischaD's avatar
MischaD committed
495
496
    def calculate_polynomials(self, approximate=True):
        """Main algorithm as proposed by D. Spiller and B Duenweg.
MischaD's avatar
MischaD committed
497

MischaD's avatar
MischaD committed
498
499
500
        Divided into the part where the existence of a solution and calculated together with all the velocity
        vectors. If one is expected the coefficients are calculated, the interval evaluated and the weight
        polynomials returned.
MischaD's avatar
MischaD committed
501

MischaD's avatar
MischaD committed
502
503
        :param approximate: Use python Fraction to approximate the calculated polynomial coefficients as rational
         number. This should give better results, since they should have a rational representation.
504

MischaD's avatar
MischaD committed
505
        :return: List of Sympy Polynomials."""
506

MischaD's avatar
MischaD committed
507
508
509
510
511
        self.solution_expected()
        self._calculate_coefficients()

        c_s_sq = sp.Symbol("c_s_sq")
        if approximate:
MischaD's avatar
MischaD committed
512
513
514
515
            self._weight_polynomials = [
                sp.Poly([approximate_ratio(x) for x in pol_coffs], c_s_sq)
                for pol_coffs in self._coefficients
            ]
MischaD's avatar
MischaD committed
516
        else:
MischaD's avatar
MischaD committed
517
518
519
            self._weight_polynomials = [
                sp.Poly(pol_coffs, c_s_sq) for pol_coffs in self._coefficients
            ]
MischaD's avatar
MischaD committed
520
521
522

        self._interval = self._calculate_valid_interval()
        return [] if self._interval == sp.EmptySet else self._weight_polynomials
523

524
    def calculate_weights(self, c_s_sq=None, boundary=None):
525
        """
526
527
528
529
530
531
        Calculate the weights for this lattice for a given speed of sound value c_s_sq, or for
        a given boundary.
        It is first checked, whether a c_s_sq value is given and then boundary.
        If both are not giving, self._boundary is taken which is defined in the
        init function.

532
533
534
535
536
        If no value is given, the infimum is used, i.e. the smallest possible value.

        :param c_s_sq: Float value for the speed of sound, has to be inside of the valid interval
        :param boundary: Get reduced model by using the infimum or supremum of the interval. Fallback value if
            no c_s_sq is given
MischaD's avatar
MischaD committed
537
        :return: weights With zero weight(s) included
538
        """
539

540
541
542
543
544
        if not self._weight_polynomials:
            self.calculate_polynomials()
        if not self._interval:  # Empty Interval
            return []
        if not c_s_sq:
545
546
            if not boundary:
                boundary = self._boundary
547
548
549
550
            if boundary == "inf":
                c_s_sq = self._interval.inf
            else:
                c_s_sq = self._interval.sup
551

552
553
554
        if self._interval.contains(c_s_sq) is sp.EmptySet:  # Invalid c_s_sq input
            return []

MischaD's avatar
MischaD committed
555
        weights = [x.eval(c_s_sq) for x in self._weight_polynomials]
MischaD's avatar
MischaD committed
556
        weights = [x if abs(x) >= 1e-10 else 0 for x in weights]
557
558
559
560
        for i, shell in enumerate(self.shells):
            shell.set_weight(c_s_sq, weights[i])
        self._c_s_sq = c_s_sq
        self._weights = weights
MischaD's avatar
MischaD committed
561
        return weights
MischaD's avatar
MischaD committed
562

563
564
    def velocity_set(self):
        """
MischaD's avatar
MischaD committed
565
566
        Removes unwanted shells, i.e. weight == = and
        returns the values that are important for the Lattice Boltzmann Model, i.e.
567
        a list of the velocity values their corresponding weights and the value for the speed of sound.
MischaD's avatar
MischaD committed
568
569

        :return: (c_s_sq, weights, velocities)
570
        """
MischaD's avatar
MischaD committed
571
572
        if not self.weights:
            self.calculate_weights()
573
574
575
576
577
578

        weights = []
        for shell in self.shells:
            if shell.weight == 0:
                continue
            weights += [shell.weight] * shell.size
MischaD's avatar
MischaD committed
579
        velocities = np.zeros((self._dimension, len(weights)), dtype=np.int32)
580
581
582
583
584
        i = 0
        for shell in self.shells:
            if shell.weight == 0:
                continue
            for velocity in shell.velocities:
MischaD's avatar
MischaD committed
585
                velocities[:, i] = velocity
586
587
588
                i += 1

        return self._c_s_sq, weights, velocities