• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

WassimTenachi / PhySO / #15

10 Jun 2024 12:47AM UTC coverage: 80.984% (+28.9%) from 52.052%
#15

push

coveralls-python

WassimTenachi
monitoring test

26 of 27 new or added lines in 1 file covered. (96.3%)

131 existing lines in 18 files now uncovered.

6814 of 8414 relevant lines covered (80.98%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.85
/physo/physym/program.py
1
import warnings as warnings
1✔
2
import numpy as np
1✔
3
import copy as copy  # for Cursor
1✔
4
import sympy as sympy
1✔
5
import pickle
1✔
6

7
# For tree image (optional)
8
import matplotlib.pyplot as plt
1✔
9
import io
1✔
10

11
# Internal imports
12
from physo.physym import token as Tok
1✔
13
from physo.physym import execute as Exec
1✔
14
from physo.physym import free_const
1✔
15

16
# Fig params
17
try:
1✔
18
    plt.rc('text', usetex=True)
1✔
19
    plt.rc('font', family='serif')
1✔
20
except:
×
21
    msg = "Not using latex font for display, as plt.rc('text', usetex=True) failed."
×
22
    warnings.warn(msg)
×
23
# Font size
24
plt.rc('font', size=16)
1✔
25

26

27
# Pickable default identity wrapper
28
def DEFAULT_WRAPPER (func, X):
1✔
29
        return func(X)
1✔
30

31
# Load pickled program
32
def load_program_pkl(fpath):
1✔
33
    """
34
    Loads program from pickle file.
35
    """
36
    with open(fpath, 'rb') as f:
1✔
37
        prog = pickle.load(f)
1✔
38
    return prog
1✔
39

40
class Cursor:
1✔
41
    """
42
    Helper class for single-token navigation in tree of programs in VectPrograms.
43
    Represents the position of a single token in a program in a batch of programs.
44
    For user-exploration, program testing and debugging.
45
    Attributes
46
    ----------
47
    programs : vect_programs.VectPrograms
48
        Batch of programs to explore.
49
    prog_idx : int
50
        Initial position of cursor in batch dim (= index of program in batch).
51
    pos : int
52
        Initial position of cursor in time dim (= index of token in program).
53
    Methods
54
    -------
55
    coords () -> numpy.array of shape (2, 1) of int
56
        Returns current coordinates in batch (batch dim, time dim) compatible with VectPrograms methods.
57
    set_pos (new_pos : int) -> program.Cursor
58
        Sets position of cursor in time dim (= index of token in program) and returns cursor.
59
    child   (i_child   : int) -> program.Cursor
60
        Returns a cursor pointing to child number i_child of current token. Raises error if there is no child.
61
    sibling (i_sibling : int) -> program.Cursor
62
        Returns a cursor pointing to sibling number i_sibling of current token. Raises error if there is no sibling .
63
        cursor.
64
    parent () -> program.Cursor
65
        Returns a cursor pointing to parent of current token. Raises error if there is no parent.
66
    """
67
    def __init__(self, programs, prog_idx=0, pos=0):
1✔
68
        """
69
        See class documentation.
70
        Parameters
71
        ----------
72
        programs : vect_programs.VectPrograms
73
        prog_idx : int
74
        pos : int
75
        """
76
        self.programs = programs
1✔
77
        self.prog_idx = prog_idx
1✔
78
        self.pos      = pos
1✔
79

80
    @property
1✔
81
    def coords(self):
1✔
82
        """
83
        See class documentation.
84
        Returns
85
        -------
86
        coords : numpy.array of shape (2, 1) of int
87
        """
88
        return np.array([[self.prog_idx], [self.pos]])
1✔
89

90
    @property
1✔
91
    def token(self):
1✔
92
        """
93
        Returns token object at coords pointed by cursor.
94
        Returns
95
        -------
96
        token : token.Token
97
        """
98
        return self.programs.get_token(self.coords)[0]
1✔
99

100
    def token_prop (self, attr):
1✔
101
        """
102
        Returns attr attribute in VectPrograms of the token at coords pointed by cursor.
103
        Returns
104
        -------
105
        token_prop : ?
106
            ? depends on the property.
107
        """
108
        return getattr(self.programs.tokens, attr)[tuple(self.coords)][0]
1✔
109

110
    def set_pos(self, new_pos = 0):
1✔
111
        """
112
        See class documentation.
113
        Parameters
114
        ----------
115
        new_pos : int
116
        Returns
117
        -------
118
        self : program.Cursor
119
        """
120
        self.pos = new_pos
1✔
121
        return self
1✔
122

123
    def child(self, i_child = 0):
1✔
124
        """
125
        See class documentation.
126
        Parameters
127
        ----------
128
        i_child : int
129
        Returns
130
        -------
131
        self : program.Cursor
132
        """
133
        has_relative     = self.programs.tokens.has_children_mask[tuple(self.coords)][0]
1✔
134
        if not has_relative:
1✔
UNCOV
135
            err_msg = "Unable to navigate to child, Token %s at pos = %i (program %i) has no child." % (
×
136
            str(self), self.pos, self.prog_idx)
137
            raise IndexError(err_msg)
×
138
        pos_children    = self.programs.get_children(tuple(self.coords))[1:, 0]
1✔
139
        child = copy.deepcopy(self)
1✔
140
        child.pos        = pos_children[i_child]
1✔
141
        return child
1✔
142

143
    @property
1✔
144
    def sibling(self, i_sibling = 0):
1✔
145
        """
146
        See class documentation.
147
        Parameters
148
        ----------
149
        i_sibling : int
150
        Returns
151
        -------
152
        self : program.Cursor
153
        """
154
        has_relative = self.programs.tokens.has_siblings_mask[tuple(self.coords)][0]
1✔
155
        if not has_relative:
1✔
UNCOV
156
            err_msg = "Unable to navigate to sibling, Token %s at pos = %i (program %i) has no sibling." % (
×
157
                str(self), self.pos, self.prog_idx)
158
            raise IndexError(err_msg)
×
159
        pos_siblings = self.programs.get_siblings(tuple(self.coords))[1:, 0]
1✔
160
        sibling = copy.deepcopy(self)
1✔
161
        sibling.pos     = pos_siblings[i_sibling]
1✔
162
        return sibling
1✔
163

164
    @property
1✔
165
    def parent(self,):
1✔
166
        """
167
        See class documentation.
168
        Returns
169
        -------
170
        self : program.Cursor
171
        """
172
        has_relative = self.programs.tokens.has_parent_mask[tuple(self.coords)][0]
1✔
173
        if not has_relative:
1✔
UNCOV
174
            err_msg = "Unable to navigate to parent, Token %s at pos = %i (program %i) has no parent." % (
×
175
                str(self), self.pos, self.prog_idx)
176
            raise IndexError(err_msg)
×
177
        pos_parent = self.programs.get_parent(tuple(self.coords))[1, 0]
1✔
178
        parent = copy.deepcopy(self)
1✔
179
        parent.pos   = pos_parent
1✔
180
        return parent
1✔
181

182
    def __repr__(self):
1✔
183
        return self.programs.lib_names[self.programs.tokens.idx[tuple(self.coords)]][0]
1✔
184

185

186
class Program:
1✔
187
    """
188
    Interface class representing a single program.
189
    Attributes
190
    ----------
191
    tokens : array_like of token.Token
192
        Tokens making up program.
193
    size : int
194
        Size of program.
195
    library : library.Library
196
        Library of tokens that could appear in Program.
197
    is_physical : bool or None
198
        Is program physical (units-wize) ?
199
    free_consts : free_const.FreeConstantsTable
200
        Free constants register for this program (having batch_size = 1).
201
        Ie. shape = (1, n_class_free_const,), (1, n_spe_free_const, n_realizations,)
202
    candidate_wrapper : callable
203
        Wrapper to apply to candidate program's output, candidate_wrapper taking func, X as arguments where func is
204
        a candidate program callable (taking X as arg). By default = None, no wrapper is applied (identity).
205
    n_realizations : int
206
        Number of realizations for this program, ie. number of datasets this program has to fit.
207
        Dataset specific free constants will have different values different for each realization.
208
        If free_consts is given, n_realizations is taken from it and this argument is ignored.
209
    """
210
    def __init__(self, tokens, library, free_consts = None, is_physical = None, candidate_wrapper = None, n_realizations=1):
1✔
211
        """
212
        Parameters
213
        ----------
214
        See attributes help for details.
215
        """
216
        # Asserting that tokens make up a full tree representation, no more, no less
217
        total_arity = np.sum([tok.arity for tok in tokens])
1✔
218
        assert len(tokens)-total_arity==1, "Tokens making up Program must consist in a full tree representation " \
1✔
219
                                           "(length - total arities = 1), no more, no less"
220
        self.tokens       = tokens
1✔
221
        self.size         = len(tokens)
1✔
222
        self.library      = library
1✔
223
        self.is_physical  = is_physical
1✔
224

225
        if candidate_wrapper is None:
1✔
226
            candidate_wrapper = DEFAULT_WRAPPER
1✔
227
        self.candidate_wrapper = candidate_wrapper
1✔
228

229
        # ----- free const related -----
230
        # If no free constants table is given, let's create one and warn the user
231
        if free_consts is None:
1✔
232
            warnings.warn("No free constants table was given when initializing prog %s, a default one will be created with initial values."%(str(self)))
1✔
233
            free_consts = free_const.FreeConstantsTable(batch_size = 1, library = library, n_realizations = n_realizations)
1✔
234

235
        self.free_consts    = free_consts             # (1, n_class_free_const,), (1, n_spe_free_const, n_realizations,)
1✔
236
        # Taking n_realizations from free_consts so if free_consts is given, n_realizations is taken from it.
237
        self.n_realizations = free_consts.n_realizations
1✔
238

239
    def execute_wo_wrapper(self, X, i_realization = 0, n_samples_per_dataset = None):
1✔
240
        """
241
        Executes program on X.
242
        Parameters
243
        ----------
244
        X : torch.tensor of shape (n_dim, ?,) of float
245
            Values of the input variables of the problem with n_dim = nb of input variables, ? = number of samples.
246
        i_realization : int, optional
247
            Index of realization to use for dataset specific free constants (0 by default).
248
        n_samples_per_dataset : array_like of shape (n_realizations,) of int or None, optional
249
            Overrides i_realization if given. If given assumes that X contains multiple datasets with samples of each
250
            dataset following each other and each portion of X corresponding to a dataset should be treated with its
251
            corresponding dataset specific free constants values. n_samples_per_dataset is the number of samples for
252
            each dataset. Eg. [90, 100, 110] for 3 datasets, this will assume that the first 90 samples of X are for
253
            the first dataset, the next 100 for the second and the last 110 for the third.
254
        Returns
255
        -------
256
        y : torch.tensor of shape (?,) of float
257
            Result of computation.
258
        """
259
        # If n_samples_per_dataset is given, we need to flatten the free constants to match the number of samples
260
        # No need to flatten if there are no spe free constants -> this could be faster
261
        # One would probably not pass n_samples_per_dataset if there are no spe free constants but physo.SR will
262
        # as SR problems are treated as Class SR problems of one realization.
263
        if self.free_consts.n_spe_free_const == 0:
1✔
264
            class_vals = self.free_consts.class_values [0]                              # (n_class_free_const,)
1✔
265
            spe_vals   = None
1✔
266
        elif n_samples_per_dataset is not None :
1✔
267
            class_const_flatten, spe_const_flatten = self.free_consts.flatten_like_data(n_samples_per_dataset=n_samples_per_dataset)
1✔
268
            # class_const_flatten                                                       # (1, n_class_free_const, ?)
269
            # spe_const_flatten                                                         # (1, n_spe_free_const,   ?)
270
            class_vals = class_const_flatten [0]                                        # (n_class_free_const, ?)
1✔
271
            spe_vals   = spe_const_flatten   [0]                                        # (n_spe_free_const,   ?)
1✔
272
        else:
273
            # self.free_consts.class_values                                             # (1, n_class_free_const,)
274
            # self.free_consts.spe_values                                               # (1, n_spe_free_const, n_realizations,)
275
            class_vals = self.free_consts.class_values[0]                               # (n_class_free_const,)
1✔
276
            spe_vals   = self.free_consts.spe_values  [0,:,i_realization]               # (n_spe_free_const,)
1✔
277

278
        y = Exec.ExecuteProgram(input_var_data         = X,
1✔
279
                                program_tokens         = self.tokens,
280
                                class_free_consts_vals = class_vals,
281
                                spe_free_consts_vals   = spe_vals,
282
                                )
283
        return y
1✔
284

285
    def execute(self, X, i_realization = 0, n_samples_per_dataset = None):
1✔
286
        """
287
        Executes program on X.
288
        Parameters
289
        ----------
290
        X : torch.tensor of shape (n_dim, ?,) of float
291
            Values of the input variables of the problem with n_dim = nb of input variables, ? = number of samples.
292
        i_realization : int, optional
293
            Index of realization to use for dataset specific free constants (0 by default).
294
        n_samples_per_dataset : array_like of shape (n_realizations,) of int or None, optional
295
            Overrides i_realization if given. If given assumes that X contains multiple datasets with samples of each
296
            dataset following each other and each portion of X corresponding to a dataset should be treated with its
297
            corresponding dataset specific free constants values. n_samples_per_dataset is the number of samples for
298
            each dataset. Eg. [90, 100, 110] for 3 datasets, this will assume that the first 90 samples of X are for
299
            the first dataset, the next 100 for the second and the last 110 for the third.
300
        Returns
301
        -------
302
        y : torch.tensor of shape (?,) of float
303
            Result of computation.
304
        """
305
        y = self.candidate_wrapper(lambda X: self.execute_wo_wrapper(X=X, i_realization=i_realization, n_samples_per_dataset=n_samples_per_dataset), X)
1✔
306
        return y
1✔
307

308
    def optimize_constants(self, X, y_target, y_weights = 1., i_realization = 0, n_samples_per_dataset = None, args_opti = None, freeze_class_free_consts = False):
1✔
309
        """
310
        Optimizes free constants of program.
311
        Parameters
312
        ----------
313
        X : torch.tensor of shape (n_dim, ?,) of float
314
            Values of the input variables of the problem with n_dim = nb of input variables, ? = number of samples.
315
        y_target : torch.tensor of shape (?,) of float
316
            Values of target output, ? = number of samples.
317
        y_weights : torch.tensor of shape (?,) of float, optional
318
            Weights for each data point.
319
        i_realization : int, optional
320
            Index of realization to use for dataset specific free constants (0 by default).
321
        n_samples_per_dataset : array_like of shape (n_realizations,) of int or None, optional
322
            Overrides i_realization if given. If given assumes that X/y_target contain multiple datasets with samples of
323
            each dataset following each other and each portion of X/y_target corresponding to a dataset should be treated
324
            with their corresponding dataset specific free constants values. n_samples_per_dataset is the number of
325
            samples for each dataset. Eg. [90, 100, 110] for 3 datasets, this will assume that the first 90 samples of X
326
            are for the first dataset, the next 100 for the second and the last 110 for the third.
327
        args_opti : dict or None, optional
328
            Arguments to pass to free_const.optimize_free_const. By default, free_const.DEFAULT_OPTI_ARGS
329
            arguments are used.
330
        freeze_class_free_consts : bool, optional
331
            If True, class free constants are not optimized.
332
        """
333
        if args_opti is None:
1✔
334
            args_opti = free_const.DEFAULT_OPTI_ARGS
1✔
335
        func_params = lambda params: self.__call__(X, i_realization=i_realization, n_samples_per_dataset=n_samples_per_dataset)
1✔
336

337
        if freeze_class_free_consts:
1✔
338
            history = free_const.optimize_free_const (  func      = func_params,
×
339
                                                        params    = [self.free_consts.spe_values],
340
                                                        y_target  = y_target,
341
                                                        y_weights = y_weights,
342
                                                        **args_opti)
343
        else:
344
            history = free_const.optimize_free_const (  func      = func_params,
1✔
345
                                                        params    = [self.free_consts.class_values, self.free_consts.spe_values],
346
                                                        y_target  = y_target,
347
                                                        y_weights = y_weights,
348
                                                        **args_opti)
349

350
        # Logging optimization process
351
        self.free_consts.is_opti    [0] = True
1✔
352
        self.free_consts.opti_steps [0] = len(history)  # Number of iterations it took to optimize the constants
1✔
353

354
        return history
1✔
355

356
    def save(self, fpath):
1✔
357
        """
358
        Saves program as a pickle file.
359
        """
360
        # Detach const data
361
        self.detach()
1✔
362
        # Save
363
        with open(fpath, 'wb') as f:
1✔
364
            pickle.dump(self, f)
1✔
365
        return None
1✔
366
    def detach(self):
1✔
367
        """
368
        Detaches program's free constants.
369
        """
370
        # Detach const data
371
        self.free_consts.detach()
1✔
372
        return self
1✔
373

374
    def make_skeleton (self):
1✔
375
        """
376
        Strips program to its bare minimum light pickable version for eg. parallel execution purposes.
377
        """
378
        # Exporting without library so it is lighter to pickle
379
        self.library             = None
1✔
380
        self.free_consts.library = None
1✔
381
        return None
1✔
382

383
    def __call__(self, X, i_realization = 0, n_samples_per_dataset=None):
1✔
384
        """
385
        Executes program on X. See Program.execute for details.
386
        """
387
        return self.execute(X = X, i_realization = i_realization, n_samples_per_dataset = n_samples_per_dataset)
1✔
388

389
    def __getitem__(self, key):
1✔
390
        """
391
        Returns token at position = key.
392
        """
393
        return self.tokens[key]
×
394

395
    def __repr__(self):
1✔
396
        return str(self.tokens)
1✔
397

398
    # ------------------------------------------------------------------------------------------------------------------
399
    # ----------------------------------------- REPRESENTATION : INFIX RELATED -----------------------------------------
400
    # ------------------------------------------------------------------------------------------------------------------
401

402
    def get_infix_str (self):
1✔
403
        """
404
        Computes infix str representation of a program.
405
        (which is the usual way to note symbolic function: +34 (in polish notation) = 3+4 (in infix notation))
406
        Returns
407
        -------
408
        program_str : str
409
        """
410
        program_str = Exec.ComputeInfixNotation(self.tokens)
1✔
411
        return program_str
1✔
412

413
    def get_sympy_local_dicts (self, replace_nan_with = 1.):
1✔
414
        """
415
        Returns a list of local dicts for each realization of the program to replace free constants by their values in
416
        sympy symbolic representation of the program.
417
        Parameters
418
        ----------
419
        replace_nan_with : float, optional
420
            Value to replace NaNs with in free constants values.
421
        Returns
422
        -------
423
        sympy_local_dicts : list of dict
424
        """
425
        sympy_local_dicts = []                                                      # (n_realizations,)
×
426
        for i_real in range(self.n_realizations):
×
427
            local_dict = {}
×
428
            class_local_dict = {self.library.class_free_constants_names[cid]: np.nan_to_num(
×
429
                self.free_consts.class_values[0][cid],
430
                nan=replace_nan_with)
431
                for cid in self.library.class_free_constants_ids}
432

433
            spe_local_dict = {self.library.spe_free_constants_names[cid]: np.nan_to_num(
×
434
                self.free_consts.spe_values[0][cid, i_real],
435
                nan=replace_nan_with)
436
                for cid in self.library.spe_free_constants_ids}
437
            local_dict.update(class_local_dict)
×
438
            local_dict.update(spe_local_dict)
×
439
            sympy_local_dicts.append(local_dict)
×
440
        return sympy_local_dicts                                                    # (n_realizations,)
×
441

442
    def get_infix_sympy (self, do_simplify = False, evaluate_consts = False, replace_nan_with = 1.):
1✔
443
        """
444
        Returns sympy symbolic representation of a program.
445
        Parameters
446
        ----------
447
        do_simplify : bool, optional
448
            If True performs a symbolic simplification of program.
449
        evaluate_consts : bool, optional
450
            If True replaces free constants by their values in the sympy symbolic representation of the program.
451
        replace_nan_with : float, optional
452
            Value to replace NaNs with in free constants values.
453
        Returns
454
        -------
455
        program_sympy : sympy.core or array of shape (n_realizations,) of sympy.core
456
            Sympy symbolic function. It is possible to run program_sympy.evalf(subs={'x': 2.4}) where 'x' is a variable
457
            appearing in the program to evaluate the function with x = 2.4.
458
            Returns an array of sympy.core if evaluate_consts is True (one for each realization as spe consts have
459
            different values for each realization).
460
        """
461
        program_str = self.get_infix_str()
1✔
462
        program_sympy = sympy.parsing.sympy_parser.parse_expr(program_str, evaluate=False)
1✔
463
        if do_simplify:
1✔
464
            program_sympy = sympy.simplify(program_sympy, rational=True) # 2.0 -> 2
1✔
465
        if evaluate_consts:
1✔
466
            sympy_local_dicts = self.get_sympy_local_dicts(replace_nan_with=replace_nan_with)                         # (n_realizations,)
×
467
            program_sympy = [ program_sympy.subs(sympy_local_dicts[i_real]) for i_real in range(self.n_realizations)] # (n_realizations,)
×
468
            if do_simplify:
×
UNCOV
469
                program_sympy = [ sympy.simplify(program_sympy[i_real], rational=True) for i_real in range(self.n_realizations)]
×
UNCOV
470
            program_sympy = np.array(program_sympy)
×
471
        return program_sympy
1✔
472

473
    def get_infix_pretty (self, do_simplify = False):
1✔
474
        """
475
        Returns a printable ASCII sympy.pretty representation of a program.
476
        Parameters
477
        ----------
478
        do_simplify : bool
479
            If True performs a symbolic simplification of program.
480
        Returns
481
        -------
482
        program_pretty_str : str
483
        """
484
        program_sympy = self.get_infix_sympy(do_simplify = do_simplify)
1✔
485
        program_pretty_str = sympy.pretty (program_sympy)
1✔
486
        return program_pretty_str
1✔
487

488

489
    def get_infix_latex (self,replace_dummy_symbol = True, new_dummy_symbol = "?", do_simplify = True):
1✔
490
        """
491
        Returns an str latex representation of a program.
492
        Parameters
493
        ----------
494
        replace_dummy_symbol : bool
495
            If True, dummy symbol is replaced by new_dummy_symbol.
496
        new_dummy_symbol : str or None
497
            Replaces dummy symbol if replace_dummy_symbol is True.
498
        do_simplify : bool
499
            If True performs a symbolic simplification of program.
500
        Returns
501
        -------
502
        program_latex_str : str
503
        """
504
        program_sympy = self.get_infix_sympy(do_simplify=do_simplify)
1✔
505
        program_latex_str = sympy.latex (program_sympy)
1✔
506
        if replace_dummy_symbol:
1✔
507
            program_latex_str = program_latex_str.replace(Tok.DUMMY_TOKEN_NAME, new_dummy_symbol)
1✔
508
        return program_latex_str
1✔
509

510

511
    def get_infix_fig (self,
1✔
512
                       replace_dummy_symbol = True,
513
                       new_dummy_symbol = "?",
514
                       do_simplify = True,
515
                       show_superparent_at_beginning = True,
516
                       text_size = 16,
517
                       text_pos  = (0.0, 0.5),
518
                       figsize   = (10, 2),
519
                       ):
520
        """
521
        Returns pyplot (figure, axis) containing analytic symbolic function program.
522
        Parameters
523
        ----------
524
        replace_dummy_symbol : bool
525
            If True, dummy symbol is replaced by new_dummy_symbol.
526
        new_dummy_symbol : str or None
527
            Replaces dummy symbol if replace_dummy_symbol is True.
528
        do_simplify : bool
529
            If True performs a symbolic simplification of program.
530
        show_superparent_at_beginning : bool
531
            If True, shows superparent in Figure like "y = ..." instead of just "..."
532
        text_size : int
533
            Size of text in figure.
534
        text_pos : (float, float)
535
            Position of text in figure.
536
        figsize : (int, int)
537
            Shape of figure.
538
        Returns
539
        -------
540
        fig, ax : matplotlib.pyplot.Figure, matplotlib.pyplot.AxesSubplot
541
        """
542
        # Latex str of symbolic function
543
        latex_str = self.get_infix_latex(replace_dummy_symbol = replace_dummy_symbol,
1✔
544
                                         new_dummy_symbol = new_dummy_symbol,
545
                                         do_simplify = do_simplify)
546
        # Adding "superparent =" before program to make it pretty
547
        if show_superparent_at_beginning:
1✔
548
            latex_str = self.library.superparent.name + ' =' + latex_str
1✔
549

550
        # Prettier fig with:
551
        #   plt.rc('text', usetex=True)
552
        #   plt.rc('font', family='serif')
553

554
        # Enables new_dummy_symbol = "\square":
555
        # plt.rc('text.latex', preamble=r'\usepackage{amssymb} \usepackage{xcolor}')
556
        if new_dummy_symbol == "\square":
1✔
557
            msg = "Use of \\square as new_dummy_symbol is not supported by matplotlib alone. " \
×
558
                  "Use plt.rc('text.latex', preamble=r'\\usepackage{amssymb} \\usepackage{xcolor}') to enable it."
559
            warnings.warn(msg)
×
560

561
        fig, ax = plt.subplots(1, 1, figsize=figsize)
1✔
562
        ax.axis('off')
1✔
563
        ax.text(text_pos[0], text_pos[1], f'${latex_str}$', size = text_size)
1✔
564
        return fig, ax
1✔
565

566

567
    def get_infix_image(self,
1✔
568
                        replace_dummy_symbol = True,
569
                        new_dummy_symbol = "?",
570
                        do_simplify = True,
571
                        text_size    = 16,
572
                        text_pos     = (0.0, 0.5),
573
                        figsize      = (8, 2),
574
                        dpi          = 512,
575
                        fpath        = None,
576
                        ):
577
        """
578
        Returns image containing analytic symbolic function program.
579
        Parameters
580
        ----------
581
        replace_dummy_symbol : bool
582
            If True, dummy symbol is replaced by new_dummy_symbol.
583
        new_dummy_symbol : str or None
584
            Replaces dummy symbol if replace_dummy_symbol is True.
585
        do_simplify : bool
586
            If True performs a symbolic simplification of program.
587
        text_size : int
588
            Size of text in figure.
589
        text_pos : (float, float)
590
            Position of text in figure.
591
        figsize : (int, int)
592
            Shape of figure.
593
        dpi : int
594
            Pixel density for raster image.
595
        fpath : str or None
596
            Path where to save image. Default = None, not saved.
597
        Returns
598
        -------
599
        image : PIL.Image.Image
600
        """
601

602
        try:
1✔
603
            import PIL as PIL
1✔
604
        except:
×
UNCOV
605
            print("Unable to import PIL (which is needed to make image data). "
×
606
                  "Please install it via 'pip install pillow >= 9.0.1'.")
607

608
        # Getting fig, ax
609
        fig, ax = self.get_infix_fig (
1✔
610
                            replace_dummy_symbol = replace_dummy_symbol,
611
                            new_dummy_symbol = new_dummy_symbol,
612
                            do_simplify = do_simplify,
613
                            text_size = text_size,
614
                            text_pos  = text_pos,
615
                            figsize   = figsize,
616
                            )
617

618
        # Exporting image to buffer
619
        buf = io.BytesIO()
1✔
620
        fig.savefig(buf, format='png', dpi=dpi)
1✔
621
        plt.close()
1✔
622

623
        # Buffer -> img
624
        white = (255, 255, 255, 255)
1✔
625
        img = PIL.Image.open(buf)
1✔
626
        bg = PIL.Image.new(img.mode, img.size, white)
1✔
627
        diff = PIL.ImageChops.difference(img, bg)
1✔
628
        diff = PIL.ImageChops.add(diff, diff, 2.0, -100)
1✔
629
        bbox = diff.getbbox()
1✔
630
        img = img.crop(bbox)
1✔
631

632
        # Saving if fpath is given
633
        if fpath is not None:
1✔
634
            fig.savefig(fpath, dpi=dpi)
×
635

636
        return img
1✔
637

638
    def show_infix(self,
1✔
639
                   replace_dummy_symbol = True,
640
                   new_dummy_symbol = "?",
641
                   do_simplify = False,
642
                   text_size=24,
643
                   text_pos=(0.0, 0.5),
644
                   figsize=(10, 1),
645
                   ):
646
        """
647
        Shows pyplot (figure, axis) containing analytic symbolic function program.
648
        Parameters
649
        ----------
650
        replace_dummy_symbol : bool
651
            If True, dummy symbol is replaced by new_dummy_symbol.
652
        new_dummy_symbol : str or None
653
            Replaces dummy symbol if replace_dummy_symbol is True.
654
        do_simplify : bool
655
            If True performs a symbolic simplification of program.
656
        text_size : int
657
            Size of text in figure.
658
        text_pos : (float, float)
659
            Position of text in figure.
660
        figsize : (int, int)
661
            Shape of figure.
662
        """
663
        # Getting fig, ax
664
        fig, ax = self.get_infix_fig (
1✔
665
                            replace_dummy_symbol = replace_dummy_symbol,
666
                            new_dummy_symbol = new_dummy_symbol,
667
                            do_simplify = do_simplify,
668
                            text_size = text_size,
669
                            text_pos  = text_pos,
670
                            figsize   = figsize,
671
                            )
672
        # Show
673
        plt.show()
1✔
674
        return None
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc