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

idaholab / MontePy / 12918569417

22 Jan 2025 10:48PM UTC coverage: 98.013% (+0.2%) from 97.855%
12918569417

Pull #608

github

MicahGale
Reved changelog to 1.0.0a1
Pull Request #608: 1.0.0 alpha to develop staging

1471 of 1487 new or added lines in 41 files covered. (98.92%)

12 existing lines in 7 files now uncovered.

7595 of 7749 relevant lines covered (98.01%)

0.98 hits per line

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

98.28
/montepy/input_parser/syntax_node.py
1
# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
2
from abc import ABC, abstractmethod
1✔
3
import collections
1✔
4
import copy
1✔
5
import itertools as it
1✔
6
import enum
1✔
7
import math
1✔
8

9
from montepy import input_parser
1✔
10
from montepy import constants
1✔
11
from montepy.constants import rel_tol, abs_tol
1✔
12
from montepy.errors import *
1✔
13
from montepy.input_parser.shortcuts import Shortcuts
1✔
14
from montepy.geometry_operators import Operator
1✔
15
from montepy.particle import Particle
1✔
16
from montepy.utilities import fortran_float
1✔
17
import re
1✔
18
import warnings
1✔
19

20

21
class SyntaxNodeBase(ABC):
1✔
22
    """
23
    A base class for all syntax nodes.
24

25
    A syntax node is any component of the syntax tree
26
    for a parsed input.
27

28
    :param name: a name for labeling this node.
29
    :type name: str
30
    """
31

32
    def __init__(self, name):
1✔
33
        self._name = name
1✔
34
        self._nodes = []
1✔
35

36
    def append(self, node):
1✔
37
        """
38
        Append the node to this node.
39

40
        :param node: node
41
        :type node: SyntaxNodeBase, str, None
42
        """
43
        self._nodes.append(node)
1✔
44

45
    @property
1✔
46
    def nodes(self):
1✔
47
        """
48
        The children nodes of this node.
49

50
        :returns: a list of the nodes.
51
        :rtype: list
52
        """
53
        return self._nodes
1✔
54

55
    def __len__(self):
1✔
56
        return len(self.nodes)
1✔
57

58
    @property
1✔
59
    def name(self):
1✔
60
        """
61
        The name for the node.
62

63
        :returns: the node's name.
64
        :rtype: str
65
        """
66
        return self._name
1✔
67

68
    @name.setter
1✔
69
    def name(self, name):
1✔
70
        if not isinstance(name, str):
1✔
71
            raise TypeError("Name must be a string")
1✔
72
        self._name = name
73

74
    @abstractmethod
75
    def format(self):
76
        """
77
        Generate a string representing the tree's current state.
78

79
        :returns: the MCNP representation of the tree's current state.
80
        :rtype: str
81
        """
82
        pass
83

84
    @property
85
    @abstractmethod
86
    def comments(self):
87
        """
88
        A generator of all comments contained in this tree.
89

90
        :returns: the comments in the tree.
91
        :rtype: Generator
92
        """
93
        pass
94

95
    def get_trailing_comment(self):
1✔
96
        """
97
        Get the trailing ``c`` style  comments if any.
98

99
        :returns: The trailing comments of this tree.
100
        :rtype: list
101
        """
102
        if len(self.nodes) == 0:
1✔
103
            return
1✔
104
        tail = self.nodes[-1]
1✔
105
        if isinstance(tail, SyntaxNodeBase):
1✔
106
            return tail.get_trailing_comment()
1✔
107

108
    def _delete_trailing_comment(self):
1✔
109
        """
110
        Deletes the trailing comment if any.
111
        """
112
        if len(self.nodes) == 0:
1✔
113
            return
1✔
114
        tail = self.nodes[-1]
1✔
115
        if isinstance(tail, SyntaxNodeBase):
1✔
116
            tail._delete_trailing_comment()
1✔
117

118
    def _grab_beginning_comment(self, extra_padding):
1✔
119
        """
120
        Consumes the provided comment, and moves it to the beginning of this node.
121

122
        :param extra_padding: the padding comment to add to the beginning of this padding.
123
        :type extra_padding: list
124
        """
125
        if len(self.nodes) == 0 or extra_padding is None:
×
126
            return
×
127
        head = self.nodes[0]
×
128
        if isinstance(head, SyntaxNodeBase):
×
129
            head._grab_beginning_comment(extra_padding)
×
130

131
    def check_for_graveyard_comments(self, has_following_input=False):
1✔
132
        """
133
        Checks if there is a graveyard comment that is preventing information from being part of the tree, and handles
134
        them.
135

136
        A graveyard comment is one that accidentally suppresses important information in the syntax tree.
137

138
        For example::
139

140
            imp:n=1 $ grave yard Vol=1
141

142
        Should be::
143

144
            imp:n=1 $ grave yard
145
            Vol=1
146

147
        These graveyards are handled by appending a new line, and the required number of continue spaces to the
148
        comment.
149

150
        .. versionadded:: 0.4.0
151

152
        :param has_following_input: Whether there is another input (cell modifier) after this tree that should be continued.
153
        :type has_following_input: bool
154
        :rtype: None
155
        """
156
        flatpack = self.flatten()
1✔
157
        if len(flatpack) == 0:
1✔
158
            return
×
159
        first = flatpack[0]
1✔
160
        if has_following_input:
1✔
161
            flatpack.append("")
1✔
162
        for second in flatpack[1:]:
1✔
163
            if isinstance(first, ValueNode):
1✔
164
                padding = first.padding
1✔
165
            elif isinstance(first, PaddingNode):
1✔
166
                padding = first
1✔
167
            else:
168
                padding = None
1✔
169
            if padding:
1✔
170
                if padding.has_graveyard_comment() and not isinstance(
1✔
171
                    second, PaddingNode
172
                ):
173
                    padding.append("\n")
1✔
174
                    padding.append(" " * constants.BLANK_SPACE_CONTINUE)
1✔
175
            first = second
1✔
176

177
    def flatten(self):
1✔
178
        """
179
        Flattens this tree structure into a list of leaves.
180

181
        .. versionadded:: 0.4.0
182

183
        :returns: a list of ValueNode and PaddingNode objects from this tree.
184
        :rtype: list
185
        """
186
        ret = []
1✔
187
        for node in self.nodes:
1✔
188
            if node is None:
1✔
189
                continue
1✔
190
            if isinstance(node, (ValueNode, PaddingNode, CommentNode, str)):
1✔
191
                ret.append(node)
1✔
192
            else:
193
                ret += node.flatten()
1✔
194
        return ret
1✔
195

196
    def _pretty_str(self):
1✔
197
        INDENT = 2
1✔
198
        if not self.nodes:
1✔
NEW
199
            return f"<Node: {self.name}: []>"
×
200
        ret = f"<Node: {self.name}: [\n"
1✔
201
        for val in self.nodes:
1✔
202
            child_strs = val._pretty_str().split("\n")
1✔
203
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[:-1]])
1✔
204
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
205
        ret += " " * INDENT + "]\n"
1✔
206
        ret += ">"
1✔
207
        return ret
1✔
208

209

210
class SyntaxNode(SyntaxNodeBase):
1✔
211
    """
212
    A general syntax node for handling inner tree nodes.
213

214
    This is a generalized wrapper for a dictionary.
215
    The order of the dictionary is significant.
216

217
    This does behave like a dict for collecting items. e.g.,
218

219
    .. code-block:: python
220

221
        value = syntax_node["start_pad"]
222
        if key in syntax_node:
223
            pass
224

225
    :param name: a name for labeling this node.
226
    :type name: str
227
    :param parse_dict: the dictionary of the syntax tree nodes.
228
    :type parse_dict: dict
229
    """
230

231
    def __init__(self, name, parse_dict):
1✔
232
        super().__init__(name)
1✔
233
        self._name = name
1✔
234
        self._nodes = parse_dict
1✔
235

236
    def __getitem__(self, key):
1✔
237
        return self.nodes[key]
1✔
238

239
    def __contains__(self, key):
1✔
240
        return key in self.nodes
1✔
241

242
    def get_value(self, key):
1✔
243
        """
244
        Get a value from the syntax tree.
245

246
        :param key: the key for the item to get.
247
        :type key: str
248
        :returns: the node in the syntax tree.
249
        :rtype: SyntaxNodeBase
250
        :raises KeyError: if key is not in SyntaxNode
251
        """
252
        temp = self.nodes[key]
1✔
253
        if isinstance(temp, ValueNode):
1✔
254
            return temp.value
1✔
255
        else:
256
            raise KeyError(f"{key} is not a value leaf node")
1✔
257

258
    def __str__(self):
1✔
259
        return f"<Node: {self.name}: {self.nodes}>"
1✔
260

261
    def __repr__(self):
1✔
262
        return str(self)
1✔
263

264
    def format(self):
1✔
265
        ret = ""
1✔
266
        for node in self.nodes.values():
1✔
267
            if isinstance(node, ValueNode):
1✔
268
                if node.value is not None:
1✔
269
                    ret += node.format()
1✔
270
            else:
271
                ret += node.format()
1✔
272
        return ret
1✔
273

274
    @property
1✔
275
    def comments(self):
1✔
276
        for node in self.nodes.values():
1✔
277
            yield from node.comments
1✔
278

279
    def get_trailing_comment(self):
1✔
280
        node = self._get_trailing_node()
1✔
281
        if node:
1✔
282
            return node.get_trailing_comment()
1✔
283

284
    def _grab_beginning_comment(self, extra_padding):
1✔
285
        """
286
        Consumes the provided comment, and moves it to the beginning of this node.
287

288
        :param extra_padding: the padding comment to add to the beginning of this padding.
289
        :type extra_padding: list
290
        """
291
        if len(self.nodes) == 0 or extra_padding is None:
×
292
            return
×
293
        head = next(iter(self.nodes.values()))
×
294
        if isinstance(head, SyntaxNodeBase):
×
295
            head._grab_beginning_comment(extra_padding)
×
296

297
    def _get_trailing_node(self):
1✔
298
        if len(self.nodes) == 0:
1✔
299
            return
×
300
        for node in reversed(self.nodes.values()):
1✔
301
            if node is not None:
1✔
302
                if isinstance(node, ValueNode):
1✔
303
                    if node.value is not None:
1✔
304
                        return node
1✔
305
                elif len(node) > 0:
1✔
306
                    return node
1✔
307

308
    def _delete_trailing_comment(self):
1✔
309
        node = self._get_trailing_node()
1✔
310
        node._delete_trailing_comment()
1✔
311

312
    def flatten(self):
1✔
313
        ret = []
1✔
314
        for node in self.nodes.values():
1✔
315
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
316
                ret.append(node)
1✔
317
            else:
318
                ret += node.flatten()
1✔
319
        return ret
1✔
320

321
    def _pretty_str(self):
1✔
322
        INDENT = 2
1✔
323
        ret = f"<Node: {self.name}: {{\n"
1✔
324
        for key, val in self.nodes.items():
1✔
325
            child_strs = val._pretty_str().split("\n")
1✔
326
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
327
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
328
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
329
        ret += " " * INDENT + "}\n"
1✔
330
        ret += ">"
1✔
331
        return ret
1✔
332

333

334
class GeometryTree(SyntaxNodeBase):
1✔
335
    """
336
    A syntax tree that is a binary tree for representing CSG geometry logic.
337

338
    .. versionchanged:: 0.4.1
339
        Added left/right_short_type
340

341
    :param name: a name for labeling this node.
342
    :type name: str
343
    :param tokens: The nodes that are in the tree.
344
    :type tokens: dict
345
    :param op: The string representation of the Operator to use.
346
    :type op: str
347
    :param left: the node of the left side of the binary tree.
348
    :type left: GeometryTree, ValueNode
349
    :param right: the node of the right side of the binary tree.
350
    :type right: GeometryTree, ValueNode
351
    :param left_short_type: The type of Shortcut that right left leaf is involved in.
352
    :type left_short_type: Shortcuts
353
    :param right_short_type: The type of Shortcut that the right leaf is involved in.
354
    :type right_short_type: Shortcuts
355
    """
356

357
    def __init__(
1✔
358
        self,
359
        name,
360
        tokens,
361
        op,
362
        left,
363
        right=None,
364
        left_short_type=None,
365
        right_short_type=None,
366
    ):
367
        super().__init__(name)
1✔
368
        assert all(list(map(lambda v: isinstance(v, SyntaxNodeBase), tokens.values())))
1✔
369
        self._nodes = tokens
1✔
370
        self._operator = Operator(op)
1✔
371
        self._left_side = left
1✔
372
        self._right_side = right
1✔
373
        self._left_short_type = left_short_type
1✔
374
        self._right_short_type = right_short_type
1✔
375

376
    def __str__(self):
1✔
377
        return (
1✔
378
            f"Geometry: < {self._left_side}"
379
            f" {f'Short:{self._left_short_type.value}' if self._left_short_type else ''}"
380
            f" {self._operator} {self._right_side} "
381
            f"{f'Short:{self._right_short_type.value}' if self._right_short_type else ''}>"
382
        )
383

384
    def _pretty_str(self):
1✔
385
        INDENT = 2
1✔
386
        ret = f"<Geometry: {self.name}: [\n"
1✔
387
        for key, val in [
1✔
388
            ("left", self._left_side._pretty_str()),
389
            ("operator", self._operator),
390
            ("right", self._right_side),
391
        ]:
392
            if val is None:
1✔
NEW
393
                continue
×
394
            if isinstance(val, SyntaxNodeBase):
1✔
395
                child_strs = val._pretty_str().split("\n")
1✔
396
            else:
397
                child_strs = [str(val)]
1✔
398
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
399
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
400
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
401
        ret += " " * INDENT + "}\n"
1✔
402
        ret += ">"
1✔
403
        return ret
1✔
404

405
    def __repr__(self):
1✔
406
        return str(self)
1✔
407

408
    def format(self):
1✔
409
        if self._left_short_type or self._right_short_type:
1✔
410
            return self._format_shortcut()
1✔
411
        ret = ""
1✔
412
        for node in self.nodes.values():
1✔
413
            ret += node.format()
1✔
414
        return ret
1✔
415

416
    def mark_last_leaf_shortcut(self, short_type):
1✔
417
        """
418
        Mark the final (rightmost) leaf node in this tree as being a shortcut.
419

420
        :param short_type: the type of shortcut that this leaf is.
421
        :type short_type: Shortcuts
422
        """
423
        if self.right is not None:
1✔
424
            node = self.right
1✔
425
            if self._right_short_type:
1✔
426
                return
1✔
427
        else:
428
            node = self.left
1✔
429
            if self._left_short_type:
1✔
430
                return
1✔
431
        if isinstance(node, type(self)):
1✔
432
            return node.mark_last_leaf_shortcut(short_type)
1✔
433
        if self.right is not None:
1✔
434
            self._right_short_type = short_type
1✔
435
        else:
436
            self._left_short_type = short_type
1✔
437

438
    def _flatten_shortcut(self):
1✔
439
        """
440
        Flattens this tree into a ListNode.
441

442
        This will add ShortcutNodes as well.
443

444
        :rtype: ListNode
445
        """
446

447
        def add_leaf(list_node, leaf, short_type):
1✔
448
            end = list_node.nodes[-1] if len(list_node) > 0 else None
1✔
449

450
            def flush_shortcut():
1✔
451
                end.load_nodes(end.nodes)
1✔
452

453
            def start_shortcut():
1✔
454
                short = ShortcutNode(short_type=short_type)
1✔
455
                # give an interpolate it's old beginning to give it right
456
                # start value
457
                if short_type in {
1✔
458
                    Shortcuts.LOG_INTERPOLATE,
459
                    Shortcuts.INTERPOLATE,
460
                } and isinstance(end, ShortcutNode):
461
                    short.append(end.nodes[-1])
1✔
462
                    short._has_pseudo_start = True
1✔
463

464
                short.append(leaf)
1✔
465
                if not leaf.padding:
1✔
466
                    leaf.padding = PaddingNode(" ")
1✔
467
                list_node.append(short)
1✔
468

469
            if short_type:
1✔
470
                if isinstance(end, ShortcutNode):
1✔
471
                    if end.type == short_type:
1✔
472
                        end.append(leaf)
1✔
473
                    else:
474
                        flush_shortcut()
1✔
475
                        start_shortcut()
1✔
476
                else:
477
                    start_shortcut()
1✔
478
            else:
479
                if isinstance(end, ShortcutNode):
1✔
480
                    flush_shortcut()
1✔
481
                    list_node.append(leaf)
1✔
482
                else:
483
                    list_node.append(leaf)
1✔
484

485
        if isinstance(self.left, ValueNode):
1✔
486
            ret = ListNode("list wrapper")
1✔
487
            add_leaf(ret, self.left, self._left_short_type)
1✔
488
        else:
489
            ret = self.left._flatten_shortcut()
1✔
490
        if self.right is not None:
1✔
491
            if isinstance(self.right, ValueNode):
1✔
492
                add_leaf(ret, self.right, self._right_short_type)
1✔
493
            else:
494
                [ret.append(n) for n in self.right._flatten_shortcut()]
1✔
495
        return ret
1✔
496

497
    def _format_shortcut(self):
1✔
498
        """
499
        Handles formatting a subset of tree that has shortcuts in it.
500
        """
501
        list_wrap = self._flatten_shortcut()
1✔
502
        if isinstance(list_wrap.nodes[-1], ShortcutNode):
1✔
503
            list_wrap.nodes[-1].load_nodes(list_wrap.nodes[-1].nodes)
1✔
504
        return list_wrap.format()
1✔
505

506
    @property
1✔
507
    def comments(self):
1✔
508
        for node in self.nodes.values():
1✔
509
            yield from node.comments
1✔
510

511
    @property
1✔
512
    def left(self):
1✔
513
        """
514
        The left side of the binary tree.
515

516
        :returns: the left node of the syntax tree.
517
        :rtype: GeometryTree, ValueNode
518
        """
519
        return self._left_side
1✔
520

521
    @property
1✔
522
    def right(self):
1✔
523
        """
524
        The right side of the binary tree.
525

526
        :returns: the right node of the syntax tree.
527
        :rtype: GeometryTree, ValueNode
528
        """
529
        return self._right_side
1✔
530

531
    @property
1✔
532
    def operator(self):
1✔
533
        """
534
        The operator used for the binary tree.
535

536
        :returns: the operator used.
537
        :rtype: Operator
538
        """
539
        return self._operator
1✔
540

541
    def __iter__(self):
1✔
542
        """
543
        Iterates over the leafs
544
        """
545
        self._iter_l_r = False
1✔
546
        self._iter_complete = False
1✔
547
        self._sub_iter = None
1✔
548
        return self
1✔
549

550
    def __next__(self):
1✔
551
        if self._iter_complete:
1✔
552
            raise StopIteration
1✔
553
        if not self._iter_l_r:
1✔
554
            node = self.left
1✔
555
        if self._iter_l_r and self.right is not None:
1✔
556
            node = self.right
1✔
557
        if isinstance(node, ValueNode):
1✔
558
            if not self._iter_l_r:
1✔
559
                if self.right is not None:
1✔
560
                    self._iter_l_r = True
1✔
561
                else:
562
                    self._iter_complete = True
1✔
563
            else:
564
                self._iter_complete = True
1✔
565
            return node
1✔
566
        if self._sub_iter is None:
1✔
567
            self._sub_iter = iter(node)
1✔
568
        try:
1✔
569
            return next(self._sub_iter)
1✔
570
        except StopIteration:
1✔
571
            self._sub_iter = None
1✔
572
            if not self._iter_l_r:
1✔
573
                if self.right is not None:
1✔
574
                    self._iter_l_r = True
1✔
575
                else:
576
                    self._iter_complete = True
1✔
577
            else:
578
                raise StopIteration
1✔
579
            return next(self)
1✔
580

581
    def flatten(self):
1✔
582
        ret = []
1✔
583
        for node in self.nodes.values():
1✔
584
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
585
                ret.append(node)
1✔
586
            else:
587
                ret += node.flatten()
1✔
588
        return ret
1✔
589

590

591
class PaddingNode(SyntaxNodeBase):
1✔
592
    """
593
    A syntax tree node to represent a collection of sequential padding elements.
594

595
    :param token: The first padding token for this node.
596
    :type token: str
597
    :param is_comment: If the token provided is a comment.
598
    :type is_comment: bool
599
    """
600

601
    def __init__(self, token=None, is_comment=False):
1✔
602
        super().__init__("padding")
1✔
603
        if token is not None:
1✔
604
            self.append(token, is_comment)
1✔
605

606
    def __str__(self):
1✔
607
        return f"<Padding, {self._nodes}>"
1✔
608

609
    def __repr__(self):
1✔
610
        return str(self)
1✔
611

612
    def __iadd__(self, other):
1✔
613
        if not isinstance(other, type(self)):
1✔
614
            raise TypeError(f"Can only combine with PaddingNodes. {other} given.")
×
615
        self._nodes += other.nodes
1✔
616
        return self
1✔
617

618
    @property
1✔
619
    def value(self):
1✔
620
        """
621
        A string representation of the contents of this node.
622

623
        All of the padding will be combined into a single string.
624

625
        :returns: a string sequence of the padding.
626
        :rtype: str
627
        """
628
        return "".join([val.format() for val in self.nodes])
1✔
629

630
    def is_space(self, i):
1✔
631
        """
632
        Determine if the value at i is a space or not.
633

634
        .. note::
635
            the newline, ``\\n``, by itself is not considered a space.
636

637
        :param i: the index of the element to check.
638
        :type i: int
639
        :returns: true iff the padding at that node is only spaces that are not ``\\n``.
640
        :raises IndexError: if the index i is not in ``self.nodes``.
641
        """
642
        val = self.nodes[i]
1✔
643
        if not isinstance(val, str):
1✔
644
            return False
1✔
645
        return len(val.strip()) == 0 and val != "\n"
1✔
646

647
    def has_space(self):
1✔
648
        """
649
        Determines if there is syntactically significant space anywhere in this node.
650

651
        :returns: True if there is syntactically significant (not in a comment) space.
652
        :rtype: bool
653
        """
654
        return any([self.is_space(i) for i in range(len(self))])
1✔
655

656
    def append(self, val, is_comment=False):
1✔
657
        """
658
        Append the node to this node.
659

660
        :param node: node
661
        :type node: str, CommentNode
662
        :param is_comment: whether or not the node is a comment.
663
        :type is_comment: bool
664
        """
665
        if is_comment and not isinstance(val, CommentNode):
1✔
666
            val = CommentNode(val)
1✔
667
        if isinstance(val, CommentNode):
1✔
668
            self.nodes.append(val)
1✔
669
            return
1✔
670
        parts = val.split("\n")
1✔
671
        if len(parts) > 1:
1✔
672
            for part in parts[:-1]:
1✔
673
                if part:
1✔
674
                    self._nodes += [part, "\n"]
1✔
675
                else:
676
                    self._nodes.append("\n")
1✔
677
            if parts[-1]:
1✔
678
                self._nodes.append(parts[-1])
1✔
679
        else:
680
            self._nodes.append(val)
1✔
681

682
    def format(self):
1✔
683
        ret = ""
1✔
684
        for node in self.nodes:
1✔
685
            if isinstance(node, str):
1✔
686
                ret += node
1✔
687
            else:
688
                ret += node.format()
1✔
689
        return ret
1✔
690

691
    @property
1✔
692
    def comments(self):
1✔
693
        for node in self.nodes:
1✔
694
            if isinstance(node, CommentNode):
1✔
695
                yield node
1✔
696

697
    def _get_first_comment(self):
1✔
698
        """
699
        Get the first index that is a ``c`` style comment.
700

701
        :returns: the index of the first comment, if there is no comment then None.
702
        :rtype: int, None
703
        """
704
        for i, item in enumerate(self.nodes):
1✔
705
            if isinstance(item, CommentNode) and not item.is_dollar:
1✔
706
                return i
1✔
707
        return None
1✔
708

709
    def get_trailing_comment(self):
1✔
710
        i = self._get_first_comment()
1✔
711
        if i is not None:
1✔
712
            return self.nodes[i:]
1✔
713
        return None
1✔
714

715
    def _delete_trailing_comment(self):
1✔
716
        i = self._get_first_comment()
1✔
717
        if i is not None:
1✔
718
            del self._nodes[i:]
1✔
719

720
    def _grab_beginning_comment(self, extra_padding):
1✔
721
        """
722
        Consumes the provided comment, and moves it to the beginning of this node.
723

724
        :param extra_padding: the padding comment to add to the beginning of this padding.
725
        :type extra_padding: list
726
        """
727
        if extra_padding[-1] != "\n":
1✔
728
            extra_padding.append("\n")
1✔
729
        self._nodes = extra_padding + self.nodes
1✔
730

731
    def __eq__(self, other):
1✔
732
        if not isinstance(other, (type(self), str)):
1✔
733
            return False
1✔
734
        if isinstance(other, type(self)):
1✔
735
            other = other.format()
1✔
736
        return self.format() == other
1✔
737

738
    def has_graveyard_comment(self):
1✔
739
        """
740
        Checks if there is a graveyard comment that is preventing information from being part of the tree.
741

742
        A graveyard comment is one that accidentally suppresses important information in the syntax tree.
743

744
        For example::
745

746
            imp:n=1 $ grave yard Vol=1
747

748
        Should be::
749

750
            imp:n=1 $ grave yard
751
            Vol=1
752

753
        .. versionadded:: 0.4.0
754

755
        :returns: True if this PaddingNode contains a graveyard comment.
756
        :rtype: bool
757
        """
758
        found = False
1✔
759
        for i, item in reversed(list(enumerate(self.nodes))):
1✔
760
            if isinstance(item, CommentNode):
1✔
761
                found = True
1✔
762
                break
1✔
763
        if not found:
1✔
764
            return False
1✔
765
        trail = self.nodes[i:]
1✔
766
        if len(trail) == 1:
1✔
767
            if trail[0].format().endswith("\n"):
1✔
768
                return False
1✔
769
            return True
1✔
770
        for node in trail[1:]:
1✔
771
            if node == "\n":
1✔
772
                return False
1✔
773
        return True
1✔
774

775

776
class CommentNode(SyntaxNodeBase):
1✔
777
    """
778
    Object to represent a comment in an MCNP problem.
779

780
    :param input: the token from the lexer
781
    :type input: Token
782
    """
783

784
    _MATCHER = re.compile(
1✔
785
        rf"""(?P<delim>
786
                (\s{{0,{constants.BLANK_SPACE_CONTINUE-1}}}C\s?)
787
                |(\$\s?)
788
             )
789
            (?P<contents>.*)""",
790
        re.I | re.VERBOSE,
791
    )
792
    """
1✔
793
    A re matcher to confirm this is a C style comment.
794
    """
795

796
    def __init__(self, input):
1✔
797
        super().__init__("comment")
1✔
798
        is_dollar, node = self._convert_to_node(input)
1✔
799
        self._is_dollar = is_dollar
1✔
800
        self._nodes = [node]
1✔
801

802
    def _convert_to_node(self, token):
1✔
803
        """
804
        Converts the token to a Syntax Node to store.
805

806
        :param token: the token to convert.
807
        :type token: str
808
        :returns: the SyntaxNode of the Comment.
809
        :rtype: SyntaxNode
810
        """
811
        if match := self._MATCHER.match(token):
1✔
812
            start = match["delim"]
1✔
813
            comment_line = match["contents"]
1✔
814
            is_dollar = "$" in start
1✔
815
        else:
816
            start = token
1✔
817
            comment_line = ""
1✔
818
            is_dollar = "$" in start
1✔
819
        return (
1✔
820
            is_dollar,
821
            SyntaxNode(
822
                "comment",
823
                {
824
                    "delimiter": ValueNode(start, str),
825
                    "data": ValueNode(comment_line, str),
826
                },
827
            ),
828
        )
829

830
    def append(self, token):
1✔
831
        """
832
        Append the comment token to this node.
833

834
        :param token: the comment token
835
        :type token: str
836
        """
837
        is_dollar, node = self._convert_to_node(token)
1✔
838
        if is_dollar or self._is_dollar:
1✔
839
            raise TypeError(
1✔
840
                f"Cannot append multiple comments to a dollar comment. {token} given."
841
            )
842
        self._nodes.append(node)
1✔
843

844
    @property
1✔
845
    def is_dollar(self):
1✔
846
        """
847
        Whether or not this CommentNode is a dollar sign ($) comment.
848

849
        :returns: True iff this is a dollar sign comment.
850
        :rtype: bool
851
        """
852
        return self._is_dollar
1✔
853

854
    @property
1✔
855
    def contents(self):
1✔
856
        """
857
        The contents of the comments without delimiters (i.e., $/C).
858

859
        :returns: String of the contents
860
        :rtype: str
861
        """
862
        return "\n".join([node["data"].value for node in self.nodes])
1✔
863

864
    def format(self):
1✔
865
        ret = ""
1✔
866
        for node in self.nodes:
1✔
867
            ret += node.format()
1✔
868
        return ret
1✔
869

870
    @property
1✔
871
    def comments(self):
1✔
872
        yield from [self]
1✔
873

874
    def __str__(self):
1✔
875
        return self.format()
1✔
876

877
    def _pretty_str(self):
1✔
NEW
878
        return str(self)
×
879

880
    def __repr__(self):
1✔
881
        ret = f"COMMENT: "
1✔
882
        for node in self.nodes:
1✔
883
            ret += node.format()
1✔
884
        return ret
1✔
885

886
    def __eq__(self, other):
1✔
887
        return str(self) == str(other)
1✔
888

889

890
class ValueNode(SyntaxNodeBase):
1✔
891
    """
892
    A syntax node to represent the leaf node.
893

894
    This stores the original input token, the current value,
895
    and the possible associated padding.
896

897
    :param token: the original token for the ValueNode.
898
    :type token: str
899
    :param token_type: the type for the ValueNode.
900
    :type token_type: class
901
    :param padding: the padding for this node.
902
    :type padding: PaddingNode
903
    :param never_pad: If true an ending space will never be added to this.
904
    :type never_pad: bool
905
    """
906

907
    _FORMATTERS = {
1✔
908
        float: {
909
            "value_length": 0,
910
            "precision": 5,
911
            "zero_padding": 0,
912
            "sign": "-",
913
            "divider": "e",
914
            "exponent_length": 0,
915
            "exponent_zero_pad": 0,
916
            "as_int": False,
917
            "int_tolerance": 1e-6,
918
            "is_scientific": True,
919
        },
920
        int: {"value_length": 0, "zero_padding": 0, "sign": "-"},
921
        str: {"value_length": 0},
922
    }
923
    """
1✔
924
    The default formatters for each type.
925
    """
926

927
    _SCIENTIFIC_FINDER = re.compile(
1✔
928
        r"""
929
            [+\-]?                      # leading sign if any
930
            (?P<significand>\d+\.*\d*)  # the actual number
931
            ((?P<e>[eE])                 # non-optional e with +/-
932
            [+\-]?|
933
            [+\-])                  #non-optional +/- if fortran float is used
934
            (?P<exponent>\d+)                    #exponent
935
        """,
936
        re.VERBOSE,
937
    )
938
    """
1✔
939
    A regex for finding scientific notation.
940
    """
941

942
    def __init__(self, token, token_type, padding=None, never_pad=False):
1✔
943
        super().__init__("")
1✔
944
        self._token = token
1✔
945
        self._type = token_type
1✔
946
        self._formatter = self._FORMATTERS[token_type].copy()
1✔
947
        self._is_neg_id = False
1✔
948
        self._is_neg_val = False
1✔
949
        self._og_value = None
1✔
950
        self._never_pad = never_pad
1✔
951
        if token is None:
1✔
952
            self._value = None
1✔
953
        elif isinstance(token, input_parser.mcnp_input.Jump):
1✔
954
            self._value = None
1✔
955
        elif token_type == float:
1✔
956
            self._value = fortran_float(token)
1✔
957
        elif token_type == int:
1✔
958
            self._value = int(token)
1✔
959
        else:
960
            self._value = token
1✔
961
        self._og_value = self.value
1✔
962
        self._padding = padding
1✔
963
        self._nodes = [self]
1✔
964
        self._is_reversed = False
1✔
965

966
    def _convert_to_int(self):
1✔
967
        """
968
        Converts a float ValueNode to an int ValueNode.
969
        """
970
        if self._type not in {float, int}:
1✔
971
            raise ValueError(f"ValueNode must be a float to convert to int")
1✔
972
        self._type = int
1✔
973
        if self._token is not None and not isinstance(
1✔
974
            self._token, input_parser.mcnp_input.Jump
975
        ):
976
            try:
1✔
977
                self._value = int(self._token)
1✔
978
            except ValueError as e:
1✔
979
                parts = self._token.split(".")
1✔
980
                if len(parts) > 1 and int(parts[1]) == 0:
1✔
981
                    self._value = int(parts[0])
1✔
982
                else:
983
                    raise e
1✔
984
        self._formatter = self._FORMATTERS[int].copy()
1✔
985

986
    def _convert_to_enum(
1✔
987
        self, enum_class, allow_none=False, format_type=str, switch_to_upper=False
988
    ):
989
        """
990
        Converts the ValueNode to an Enum for allowed values.
991

992
        :param enum_class: the class for the enum to use.
993
        :type enum_class: Class
994
        :param allow_none: Whether or not to allow None as a value.
995
        :type allow_none: bool
996
        :param format_type: the base data type to format this ValueNode as.
997
        :type format_type: Class
998
        :param switch_to_upper: Whether or not to convert a string to upper case before convert to enum.
999
        :type switch_to_upper: bool
1000
        """
1001
        self._type = enum_class
1✔
1002
        if switch_to_upper:
1✔
1003
            value = self._value.upper()
1✔
1004
        else:
1005
            value = self._value
1✔
1006
        if not (allow_none and self._value is None):
1✔
1007
            self._value = enum_class(value)
1✔
1008
        self._formatter = self._FORMATTERS[format_type].copy()
1✔
1009

1010
    @property
1✔
1011
    def is_negatable_identifier(self):
1✔
1012
        """
1013
        Whether or not this value is a negatable identifier.
1014

1015
        Example use: the surface transform or periodic surface is switched based on positive
1016
        or negative.
1017

1018
        This means:
1019
            1. the ValueNode is an int.
1020
            2. The ``value`` will always be positive.
1021
            3. The ``is_negative`` property will be available.
1022

1023
        :returns: the state of this marker.
1024
        :rtype: bool
1025
        """
1026
        return self._is_neg_id
1✔
1027

1028
    @is_negatable_identifier.setter
1✔
1029
    def is_negatable_identifier(self, val):
1✔
1030
        if val == True:
1✔
1031
            self._convert_to_int()
1✔
1032
            if self.value is not None:
1✔
1033
                self._is_neg = self.value < 0
1✔
1034
                self._value = abs(self._value)
1✔
1035
            else:
1036
                self._is_neg = None
1✔
1037
        self._is_neg_id = val
1✔
1038

1039
    @property
1✔
1040
    def is_negatable_float(self):
1✔
1041
        """
1042
        Whether or not this value is a negatable float.
1043

1044
        Example use: cell density.
1045

1046
        This means:
1047
            1. the ValueNode is an int.
1048
            2. The ``value`` will always be positive.
1049
            3. The ``is_negative`` property will be available.
1050

1051
        :returns: the state of this marker.
1052
        :rtype: bool
1053
        """
1054
        return self._is_neg_val
1✔
1055

1056
    @is_negatable_float.setter
1✔
1057
    def is_negatable_float(self, val):
1✔
1058
        if val == True:
1✔
1059
            if self.value is not None:
1✔
1060
                self._is_neg = self.value < 0
1✔
1061
                self._value = abs(self._value)
1✔
1062
            else:
1063
                self._is_neg = None
1✔
1064
        self._is_neg_val = val
1✔
1065

1066
    @property
1✔
1067
    def is_negative(self):
1✔
1068
        """
1069
        Whether or not this value is negative.
1070

1071
        If neither :func:`is_negatable_float` or :func:`is_negatable_identifier` is true
1072
        then this will return ``None``.
1073

1074
        :returns: true if this value is negative (either in input or through state).
1075
        :rtype: bool, None
1076
        """
1077
        if self.is_negatable_identifier or self.is_negatable_float:
1✔
1078
            return self._is_neg
1✔
1079

1080
    @is_negative.setter
1✔
1081
    def is_negative(self, val):
1✔
1082
        if self.is_negatable_identifier or self.is_negatable_float:
1✔
1083
            self._is_neg = val
1✔
1084

1085
    def _reverse_engineer_formatting(self):
1✔
1086
        """
1087
        Tries its best to figure out and update the formatter based on the token's format.
1088
        """
1089
        if not self._is_reversed and self._token is not None:
1✔
1090
            self._is_reversed = True
1✔
1091
            token = self._token
1✔
1092
            if isinstance(token, input_parser.mcnp_input.Jump):
1✔
1093
                token = "J"
1✔
1094
            if isinstance(token, (int, float)):
1✔
1095
                token = str(token)
1✔
1096
            self._formatter["value_length"] = len(token)
1✔
1097
            if self.padding:
1✔
1098
                if self.padding.is_space(0):
1✔
1099
                    self._formatter["value_length"] += len(self.padding.nodes[0])
1✔
1100

1101
            if self._type == float or self._type == int:
1✔
1102
                no_zero_pad = token.lstrip("0+-")
1✔
1103
                length = len(token)
1✔
1104
                delta = length - len(no_zero_pad)
1✔
1105
                if token.startswith("+") or token.startswith("-"):
1✔
1106
                    delta -= 1
1✔
1107
                    if token.startswith("+"):
1✔
1108
                        self._formatter["sign"] = "+"
1✔
1109
                    if token.startswith("-") and not self.never_pad:
1✔
1110
                        self._formatter["sign"] = " "
1✔
1111
                if delta > 0:
1✔
1112
                    self._formatter["zero_padding"] = length
1✔
1113
                if self._type == float:
1✔
1114
                    self._reverse_engineer_float()
1✔
1115

1116
    def _reverse_engineer_float(self):
1✔
1117
        token = self._token
1✔
1118
        if isinstance(token, float):
1✔
1119
            token = str(token)
1✔
1120
        if isinstance(token, input_parser.mcnp_input.Jump):
1✔
1121
            token = "J"
1✔
1122
        if match := self._SCIENTIFIC_FINDER.match(token):
1✔
1123
            groups = match.groupdict(default="")
1✔
1124
            self._formatter["is_scientific"] = True
1✔
1125
            significand = groups["significand"]
1✔
1126
            self._formatter["divider"] = groups["e"]
1✔
1127
            # extra space for the "e" in scientific and... stuff
1128
            self._formatter["zero_padding"] += 4
1✔
1129
            exponent = groups["exponent"]
1✔
1130
            temp_exp = exponent.lstrip("0")
1✔
1131
            if exponent != temp_exp:
1✔
1132
                self._formatter["exponent_length"] = len(exponent)
1✔
1133
                self._formatter["exponent_zero_pad"] = len(exponent)
1✔
1134
        else:
1135
            self._formatter["is_scientific"] = False
1✔
1136
            significand = token
1✔
1137
        parts = significand.split(".")
1✔
1138
        if len(parts) == 2:
1✔
1139
            precision = len(parts[1])
1✔
1140
        else:
1141
            precision = self._FORMATTERS[float]["precision"]
1✔
1142
            self._formatter["as_int"] = True
1✔
1143

1144
        self._formatter["precision"] = precision
1✔
1145

1146
    def _can_float_to_int_happen(self):
1✔
1147
        """
1148
        Checks if you can format a floating point as an int.
1149

1150
        E.g., 1.0 -> 1
1151

1152
        Considers if this was done in the input, and if the value is close to the int value.
1153

1154
        :rtype: bool.
1155
        """
1156
        if self._type != float or not self._formatter["as_int"]:
1✔
1157
            return False
1✔
1158
        nearest_int = round(self.value)
1✔
1159
        if not math.isclose(nearest_int, self.value, rel_tol=rel_tol, abs_tol=abs_tol):
1✔
1160
            return False
1✔
1161
        return True
1✔
1162

1163
    @property
1✔
1164
    def _print_value(self):
1✔
1165
        """
1166
        The print version of the value.
1167

1168
        This takes a float/int that is negatable, and negates it
1169
        based on the ``is_negative`` value.
1170

1171
        :rtype: int, float
1172
        """
1173
        if self._type in {int, float} and self.is_negative:
1✔
1174
            return -self.value
1✔
1175
        return self.value
1✔
1176

1177
    @property
1✔
1178
    def _value_changed(self):
1✔
1179
        """
1180
        Checks if the value has changed at all from first parsing.
1181

1182
        Used to shortcut formatting and reverse engineering.
1183

1184
        :rtype: bool
1185
        """
1186
        if self.value is None and self._og_value is None:
1✔
1187
            return False
1✔
1188
        if self.value is None or self._og_value is None:
1✔
1189
            return True
1✔
1190
        if self._type in {float, int}:
1✔
1191
            return not math.isclose(
1✔
1192
                self._print_value, self._og_value, rel_tol=rel_tol, abs_tol=abs_tol
1193
            )
1194
        return self.value != self._og_value
1✔
1195

1196
    def format(self):
1✔
1197
        if not self._value_changed:
1✔
1198
            return f"{self._token}{self.padding.format() if self.padding else ''}"
1✔
1199
        if self.value is None:
1✔
1200
            return ""
1✔
1201
        self._reverse_engineer_formatting()
1✔
1202
        if issubclass(self.type, enum.Enum):
1✔
1203
            value = self.value.value
1✔
1204
        else:
1205
            value = self._print_value
1✔
1206
        if self._type == int or self._can_float_to_int_happen():
1✔
1207
            temp = "{value:0={sign}{zero_padding}d}".format(
1✔
1208
                value=int(value), **self._formatter
1209
            )
1210
        elif self._type == float:
1✔
1211
            # default to python general if new value
1212
            if not self._is_reversed:
1✔
1213
                temp = "{value:0={sign}{zero_padding}.{precision}g}".format(
1✔
1214
                    value=value, **self._formatter
1215
                )
1216
            elif self._formatter["is_scientific"]:
1✔
1217
                temp = "{value:0={sign}{zero_padding}.{precision}e}".format(
1✔
1218
                    value=value, **self._formatter
1219
                )
1220
                temp = temp.replace("e", self._formatter["divider"])
1✔
1221
                temp_match = self._SCIENTIFIC_FINDER.match(temp)
1✔
1222
                exponent = temp_match.group("exponent")
1✔
1223
                start, end = temp_match.span("exponent")
1✔
1224
                new_exp_temp = "{value:0={zero_padding}d}".format(
1✔
1225
                    value=int(exponent),
1226
                    zero_padding=self._formatter["exponent_zero_pad"],
1227
                )
1228
                new_exp = "{temp:<{value_length}}".format(
1✔
1229
                    temp=new_exp_temp, value_length=self._formatter["exponent_length"]
1230
                )
1231
                temp = temp[0:start] + new_exp + temp[end:]
1✔
1232
            elif self._formatter["as_int"]:
1✔
1233
                temp = "{value:0={sign}0{zero_padding}g}".format(
1✔
1234
                    value=value, **self._formatter
1235
                )
1236
            else:
1237
                temp = "{value:0={sign}0{zero_padding}.{precision}f}".format(
1✔
1238
                    value=value, **self._formatter
1239
                )
1240
        else:
1241
            temp = str(value)
1✔
1242
        if self.padding:
1✔
1243
            if self.padding.is_space(0):
1✔
1244
                # if there was and end space, and we ran out of space, and there isn't
1245
                # a saving space later on
1246
                if len(temp) >= self._formatter["value_length"] and not (
1✔
1247
                    len(self.padding) > 1
1248
                    and (self.padding.is_space(1) or self.padding.nodes[1] == "\n")
1249
                ):
1250
                    pad_str = " "
1✔
1251
                else:
1252
                    pad_str = ""
1✔
1253
                extra_pad_str = "".join([x.format() for x in self.padding.nodes[1:]])
1✔
1254
            else:
1255
                pad_str = ""
1✔
1256
                extra_pad_str = "".join([x.format() for x in self.padding.nodes])
1✔
1257
        else:
1258
            pad_str = ""
1✔
1259
            extra_pad_str = ""
1✔
1260
        buffer = "{temp:<{value_length}}{padding}".format(
1✔
1261
            temp=temp, padding=pad_str, **self._formatter
1262
        )
1263
        if len(buffer) > self._formatter["value_length"] and self._token is not None:
1✔
1264
            warning = LineExpansionWarning(
1✔
1265
                f"The value has expanded, and may change formatting. The original value was {self._token}, new value is {temp}."
1266
            )
1267
            warning.cause = "value"
1✔
1268
            warning.og_value = self._token
1✔
1269
            warning.new_value = temp
1✔
1270
            warnings.warn(
1✔
1271
                warning,
1272
                stacklevel=2,
1273
            )
1274
        return buffer + extra_pad_str
1✔
1275

1276
    @property
1✔
1277
    def comments(self):
1✔
1278
        if self.padding is not None:
1✔
1279
            yield from self.padding.comments
1✔
1280
        else:
1281
            yield from []
1✔
1282

1283
    def get_trailing_comment(self):
1✔
1284
        if self.padding is None:
1✔
1285
            return
1✔
1286
        return self.padding.get_trailing_comment()
1✔
1287

1288
    def _delete_trailing_comment(self):
1✔
1289
        if self.padding is None:
1✔
1290
            return
1✔
1291
        self.padding._delete_trailing_comment()
1✔
1292

1293
    @property
1✔
1294
    def padding(self):
1✔
1295
        """
1296
        The padding if any for this ValueNode.
1297

1298
        :returns: the padding if any.
1299
        :rtype: PaddingNode
1300
        """
1301
        return self._padding
1✔
1302

1303
    @padding.setter
1✔
1304
    def padding(self, pad):
1✔
1305
        self._padding = pad
1✔
1306

1307
    @property
1✔
1308
    def type(self):
1✔
1309
        """
1310
        The data type for this ValueNode.
1311

1312
        Examples: float, int, str, Lattice
1313

1314
        :returns: the class for the value of this node.
1315
        :rtype: Class
1316
        """
1317
        return self._type
1✔
1318

1319
    @property
1✔
1320
    def token(self):
1✔
1321
        """
1322
        The original text (token) for this ValueNode.
1323

1324
        :returns: the original input.
1325
        :rtype: str
1326
        """
1327
        return self._token
1✔
1328

1329
    def __str__(self):
1✔
1330
        return f"<Value, {self._value}, padding: {self._padding}>"
1✔
1331

1332
    def _pretty_str(self):
1✔
1333
        return str(self)
1✔
1334

1335
    def __repr__(self):
1✔
1336
        return str(self)
1✔
1337

1338
    @property
1✔
1339
    def value(self):
1✔
1340
        """
1341
        The current semantic value of this ValueNode.
1342

1343
        This is the parsed meaning in the type of ``self.type``,
1344
        that can be updated. When this value is updated, next time format()
1345
        is ran this value will be used.
1346

1347
        :returns: the node's value in type ``type``.
1348
        :rtype: float, int, str, enum
1349
        """
1350
        return self._value
1✔
1351

1352
    @property
1✔
1353
    def never_pad(self):
1✔
1354
        """
1355
        Whether or not this value node will not have extra spaces added.
1356

1357
        :returns: true if extra padding is not adding at the end if missing.
1358
        :rtype: bool
1359
        """
1360
        return self._never_pad
1✔
1361

1362
    @never_pad.setter
1✔
1363
    def never_pad(self, never_pad):
1✔
1364
        self._never_pad = never_pad
1✔
1365

1366
    @value.setter
1✔
1367
    def value(self, value):
1✔
1368
        if self.is_negative is not None and value is not None:
1✔
1369
            value = abs(value)
1✔
1370
        self._check_if_needs_end_padding(value)
1✔
1371
        self._value = value
1✔
1372

1373
    def _check_if_needs_end_padding(self, value):
1✔
1374
        if value is None or self.value is not None or self._never_pad:
1✔
1375
            return
1✔
1376
        # if not followed by a trailing space
1377
        if self.padding is None:
1✔
1378
            self.padding = PaddingNode(" ")
1✔
1379

1380
    def __eq__(self, other):
1✔
1381
        if not isinstance(other, (type(self), str, int, float)):
1✔
1382
            return False
1✔
1383
        if isinstance(other, ValueNode):
1✔
1384
            other_val = other.value
1✔
1385
            if self.type != other.type:
1✔
1386
                return False
1✔
1387
        else:
1388
            other_val = other
1✔
1389
            if self.type != type(other):
1✔
1390
                return False
1✔
1391

1392
        if self.type == float and self.value is not None and other_val is not None:
1✔
1393
            return math.isclose(self.value, other_val, rel_tol=rel_tol, abs_tol=abs_tol)
1✔
1394
        return self.value == other_val
1✔
1395

1396

1397
class ParticleNode(SyntaxNodeBase):
1✔
1398
    """
1399
    A node to hold particles information in a :class:`ClassifierNode`.
1400

1401
    :param name: the name for the node.
1402
    :type name: str
1403
    :param token: the original token from parsing
1404
    :type token: str
1405
    """
1406

1407
    _letter_finder = re.compile(r"([a-zA-Z])")
1✔
1408

1409
    def __init__(self, name, token):
1✔
1410
        super().__init__(name)
1✔
1411
        self._nodes = [self]
1✔
1412
        self._token = token
1✔
1413
        self._order = []
1✔
1414
        classifier_chunks = token.replace(":", "").split(",")
1✔
1415
        self._particles = set()
1✔
1416
        self._formatter = {"upper": False}
1✔
1417
        for chunk in classifier_chunks:
1✔
1418
            part = Particle(chunk.upper())
1✔
1419
            self._particles.add(part)
1✔
1420
            self._order.append(part)
1✔
1421

1422
    @property
1✔
1423
    def token(self):
1✔
1424
        """
1425
        The original text (token) for this ParticleNode.
1426

1427
        :returns: the original input.
1428
        :rtype: str
1429
        """
1430
        return self._token
1✔
1431

1432
    @property
1✔
1433
    def particles(self):
1✔
1434
        """
1435
        The particles included in this node.
1436

1437
        :returns: a set of the particles being used.
1438
        :rtype: set
1439
        """
1440
        return self._particles
1✔
1441

1442
    @particles.setter
1✔
1443
    def particles(self, values):
1✔
1444
        if not isinstance(values, (list, set)):
1✔
1445
            raise TypeError(f"Particles must be a set. {values} given.")
1✔
1446
        for value in values:
1✔
1447
            if not isinstance(value, Particle):
1✔
1448
                raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1449
        if isinstance(values, list):
1✔
1450
            self._order = values
1✔
1451
            values = set(values)
1✔
1452
        self._particles = values
1✔
1453

1454
    def add(self, value):
1✔
1455
        """
1456
        Add a particle to this node.
1457

1458
        :param value: the particle to add.
1459
        :type value: Particle
1460
        """
1461
        if not isinstance(value, Particle):
1✔
1462
            raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1463
        self._order.append(value)
1✔
1464
        self._particles.add(value)
1✔
1465

1466
    def remove(self, value):
1✔
1467
        """
1468
        Remove a particle from this node.
1469

1470
        :param value: the particle to remove.
1471
        :type value: Particle
1472
        """
1473
        if not isinstance(value, Particle):
1✔
1474
            raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1475
        self._particles.remove(value)
1✔
1476
        self._order.remove(value)
1✔
1477

1478
    @property
1✔
1479
    def _particles_sorted(self):
1✔
1480
        """
1481
        The particles in this node ordered in a nice-ish way.
1482

1483
        Ordering:
1484
            1. User input.
1485
            2. Order of particles appended
1486
            3. randomly at the end if all else fails.
1487

1488
        :rtype: list
1489
        """
1490
        ret = self._order
1✔
1491
        ret_set = set(ret)
1✔
1492
        remainder = self.particles - ret_set
1✔
1493
        extras = ret_set - self.particles
1✔
1494
        for straggler in sorted(remainder):
1✔
1495
            ret.append(straggler)
1✔
1496
        for useless in extras:
1✔
1497
            ret.remove(useless)
1✔
1498
        return ret
1✔
1499

1500
    def format(self):
1✔
1501
        self._reverse_engineer_format()
1✔
1502
        if self._formatter["upper"]:
1✔
1503
            parts = [p.value.upper() for p in self._particles_sorted]
1✔
1504
        else:
1505
            parts = [p.value.lower() for p in self._particles_sorted]
1✔
1506
        return f":{','.join(parts)}"
1✔
1507

1508
    def _reverse_engineer_format(self):
1✔
1509
        total_match = 0
1✔
1510
        upper_match = 0
1✔
1511
        for match in self._letter_finder.finditer(self._token):
1✔
1512
            if match:
1✔
1513
                if match.group(0).isupper():
1✔
1514
                    upper_match += 1
1✔
1515
                total_match += 1
1✔
1516
        if upper_match / total_match >= 0.5:
1✔
1517
            self._formatter["upper"] = True
1✔
1518

1519
    @property
1✔
1520
    def comments(self):
1✔
1521
        yield from []
1✔
1522

1523
    def __repr__(self):
1✔
1524
        return self.format()
1✔
1525

1526
    def __iter__(self):
1✔
1527
        return iter(self.particles)
1✔
1528

1529

1530
class ListNode(SyntaxNodeBase):
1✔
1531
    """
1532
    A node to represent a list of values.
1533

1534
    :param name: the name of this node.
1535
    :type name: str
1536
    """
1537

1538
    def __init__(self, name):
1✔
1539
        super().__init__(name)
1✔
1540
        self._shortcuts = []
1✔
1541

1542
    def __repr__(self):
1✔
1543
        return f"(list: {self.name}, {self.nodes})"
1✔
1544

1545
    def update_with_new_values(self, new_vals):
1✔
1546
        """
1547
        Update this list node with new values.
1548

1549
        This will first try to find if any shortcuts in the original input match up with
1550
        the new values. If so it will then "zip" out those shortcuts to consume
1551
        as many neighbor nodes as possible.
1552
        Finally, the internal shortcuts, and list will be updated to reflect the new state.
1553

1554
        :param new_vals: the new values (a list of ValueNodes)
1555
        :type new_vals: list
1556
        """
1557
        if not new_vals:
1✔
1558
            self._nodes = []
×
1559
            return
×
1560
        new_vals_cache = {id(v): v for v in new_vals}
1✔
1561
        # bind shortcuts to single site in new values
1562
        for shortcut in self._shortcuts:
1✔
1563
            for node in shortcut.nodes:
1✔
1564
                if id(node) in new_vals_cache:
1✔
1565
                    new_vals_cache[id(node)] = shortcut
1✔
1566
                    shortcut.nodes.clear()
1✔
1567
                    break
1✔
1568
        self._expand_shortcuts(new_vals, new_vals_cache)
1✔
1569
        self._shortcuts = []
1✔
1570
        self._nodes = []
1✔
1571
        for key, node in new_vals_cache.items():
1✔
1572
            if isinstance(node, ShortcutNode):
1✔
1573
                if (
1✔
1574
                    len(self._shortcuts) > 0 and node is not self._shortcuts[-1]
1575
                ) or len(self._shortcuts) == 0:
1576
                    self._shortcuts.append(node)
1✔
1577
                    self._nodes.append(node)
1✔
1578
            else:
1579
                self._nodes.append(node)
1✔
1580
        end = self._nodes[-1]
1✔
1581
        # pop off final shortcut if it's a jump the user left off
1582
        if (
1✔
1583
            isinstance(end, ShortcutNode)
1584
            and end._type == Shortcuts.JUMP
1585
            and len(end._original) == 0
1586
        ):
1587
            self._nodes.pop()
1✔
1588
            self._shortcuts.pop()
1✔
1589

1590
    def _expand_shortcuts(self, new_vals, new_vals_cache):
1✔
1591
        """
1592
        Expands the existing shortcuts, and tries to "zip out" and consume their neighbors.
1593

1594
        :param new_vals: the new values.
1595
        :type new_vals: list
1596
        :param new_vals_cache: a dictionary mapping the id of the ValueNode to the ValueNode
1597
            or ShortcutNode. This is ordered the same as ``new_vals``.
1598
        :type new_vals_cache: dict
1599
        """
1600

1601
        def try_expansion(shortcut, value):
1✔
1602
            status = shortcut.consume_edge_node(
1✔
1603
                value, 1, i == last_end + 1 and last_end != 0
1604
            )
1605
            if status:
1✔
1606
                new_vals_cache[id(value)] = shortcut
1✔
1607
            else:
1608
                new_vals_cache[id(value)] = value
1✔
1609
            return status
1✔
1610

1611
        def try_reverse_expansion(shortcut, i, last_end):
1✔
1612
            if i > 1:
1✔
1613
                for value in new_vals[i - 1 : last_end : -1]:
1✔
1614
                    if shortcut.consume_edge_node(value, -1):
1✔
1615
                        new_vals_cache[id(value)] = shortcut
1✔
1616
                    else:
1617
                        new_vals_cache[id(value)] = value
1✔
1618
                        return
1✔
1619

1620
        def check_for_orphan_jump(value):
1✔
1621
            """
1622
            Checks if the current Jump is not tied to an existing Shortcut
1623
            """
1624
            nonlocal shortcut
1625
            if value.value is None and shortcut is None:
1✔
1626
                shortcut = ShortcutNode(p=None, short_type=Shortcuts.JUMP)
1✔
1627
                if shortcut.consume_edge_node(value, 1):
1✔
1628
                    new_vals_cache[id(value)] = shortcut
1✔
1629

1630
        shortcut = None
1✔
1631
        last_end = 0
1✔
1632
        for i, value in enumerate(new_vals_cache.values()):
1✔
1633
            # found a new shortcut
1634
            if isinstance(value, ShortcutNode):
1✔
1635
                # shortcuts bumped up against each other
1636
                if shortcut is not None:
1✔
1637
                    last_end = i - 1
1✔
1638
                shortcut = value
1✔
1639
                if try_expansion(shortcut, new_vals[i]):
1✔
1640
                    try_reverse_expansion(shortcut, i, last_end)
1✔
1641
                else:
1642
                    shortcut = None
×
1643
            # otherwise it is actually a value to expand as well
1644
            else:
1645
                if shortcut is not None:
1✔
1646
                    if not try_expansion(shortcut, new_vals[i]):
1✔
1647
                        last_end = i - 1
1✔
1648
                        shortcut = None
1✔
1649
                        check_for_orphan_jump(new_vals[i])
1✔
1650
                else:
1651
                    check_for_orphan_jump(new_vals[i])
1✔
1652

1653
    def append(self, val, from_parsing=False):
1✔
1654
        """
1655
        Append the node to this node.
1656

1657
        :param node: node
1658
        :type node: ValueNode, ShortcutNode
1659
        :param from_parsing: If this is being append from the parsers, and not elsewhere.
1660
        :type from_parsing: bool
1661
        """
1662
        if isinstance(val, ShortcutNode):
1✔
1663
            self._shortcuts.append(val)
1✔
1664
        if len(self) > 0 and from_parsing:
1✔
1665
            last = self[-1]
1✔
1666
            if isinstance(last, ValueNode) and (
1✔
1667
                (last.padding and not last.padding.has_space) or last.padding is None
1668
            ):
1669
                self[-1].never_pad = True
1✔
1670
        super().append(val)
1✔
1671

1672
    @property
1✔
1673
    def comments(self):
1✔
1674
        for node in self.nodes:
1✔
1675
            yield from node.comments
1✔
1676

1677
    def format(self):
1✔
1678
        ret = ""
1✔
1679
        length = len(self.nodes)
1✔
1680
        last_node = None
1✔
1681
        for i, node in enumerate(self.nodes):
1✔
1682
            # adds extra padding
1683
            if (
1✔
1684
                isinstance(node, ValueNode)
1685
                and node.padding is None
1686
                and i < length - 1
1687
                and not isinstance(self.nodes[i + 1], PaddingNode)
1688
                and not node.never_pad
1689
            ):
1690
                node.padding = PaddingNode(" ")
1✔
1691
            if isinstance(last_node, ShortcutNode) and isinstance(node, ShortcutNode):
1✔
1692
                ret += node.format(last_node)
1✔
1693
            else:
1694
                ret += node.format()
1✔
1695
            last_node = node
1✔
1696
        return ret
1✔
1697

1698
    def __iter__(self):
1✔
1699
        for node in self.nodes:
1✔
1700
            if isinstance(node, ShortcutNode):
1✔
1701
                yield from node.nodes
1✔
1702
            else:
1703
                yield node
1✔
1704

1705
    def __contains__(self, value):
1✔
1706
        for node in self:
1✔
1707
            if node == value:
1✔
1708
                return True
1✔
1709
        return False
1✔
1710

1711
    def __getitem__(self, indx):
1✔
1712
        if isinstance(indx, slice):
1✔
1713
            return self.__get_slice(indx)
1✔
1714
        if indx >= 0:
1✔
1715
            for i, item in enumerate(self):
1✔
1716
                if i == indx:
1✔
1717
                    return item
1✔
1718
        else:
1719
            items = list(self)
1✔
1720
            return items[indx]
1✔
1721
        raise IndexError(f"{indx} not in ListNode")
1✔
1722

1723
    def __get_slice(self, i: slice):
1✔
1724
        """
1725
        Helper function for __getitem__ with slices.
1726
        """
1727
        rstep = i.step if i.step is not None else 1
1✔
1728
        rstart = i.start
1✔
1729
        rstop = i.stop
1✔
1730
        if rstep < 0:  # Backwards
1✔
1731
            if rstart is None:
1✔
1732
                rstart = len(self.nodes) - 1
1✔
1733
            if rstop is None:
1✔
1734
                rstop = 0
1✔
1735
            rstop -= 1
1✔
1736
        else:  # Forwards
1737
            if rstart is None:
1✔
1738
                rstart = 0
1✔
1739
            if rstop is None:
1✔
1740
                rstop = len(self.nodes) - 1
1✔
1741
            rstop += 1
1✔
1742
        buffer = []
1✔
1743
        allowed_indices = range(rstart, rstop, rstep)
1✔
1744
        for i, item in enumerate(self):
1✔
1745
            if i in allowed_indices:
1✔
1746
                buffer.append(item)
1✔
1747
        ret = ListNode(f"{self.name}_slice")
1✔
1748
        if rstep < 0:
1✔
1749
            buffer.reverse()
1✔
1750
        for val in buffer:
1✔
1751
            ret.append(val)
1✔
1752
        return ret
1✔
1753

1754
    def remove(self, obj):
1✔
1755
        """
1756
        Removes the given object from this list.
1757

1758
        :param obj: the object to remove.
1759
        :type obj: ValueNode
1760
        """
1761
        self.nodes.remove(obj)
1✔
1762

1763
    def __eq__(self, other):
1✔
1764
        if not isinstance(other, (type(self), list)):
1✔
1765
            return False
1✔
1766
        if len(self) != len(other):
1✔
1767
            return False
1✔
1768
        for lhs, rhs in zip(self, other):
1✔
1769
            if lhs != rhs:
1✔
1770
                return False
1✔
1771
        return True
1✔
1772

1773

1774
class MaterialsNode(SyntaxNodeBase):
1✔
1775
    """
1776
    A node for representing isotopes and their concentration,
1777
    and the material parameters.
1778

1779
    This stores a list of tuples of ZAIDs and concentrations,
1780
    or a tuple of a parameter.
1781

1782
    .. versionadded:: 1.0.0
1783

1784
        This was added as a more general version of ``IsotopesNodes``.
1785

1786
    :param name: a name for labeling this node.
1787
    :type name: str
1788
    """
1789

1790
    def __init__(self, name):
1✔
1791
        super().__init__(name)
1✔
1792

1793
    def append_nuclide(self, isotope_fraction):
1✔
1794
        """
1795
        Append the isotope fraction to this node.
1796

1797
        .. versionadded:: 1.0.0
1798

1799
            Added to replace ``append``
1800

1801
        :param isotope_fraction: the isotope_fraction to add. This must be a tuple from
1802
            A Yacc production. This will consist of: the string identifying the Yacc production,
1803
            a ValueNode that is the ZAID, and a ValueNode of the concentration.
1804
        :type isotope_fraction: tuple
1805
        """
1806
        isotope, concentration = isotope_fraction[1:3]
1✔
1807
        self._nodes.append((isotope, concentration))
1✔
1808

1809
    def append(self):  # pragma: no cover
1810
        raise DeprecationWarning("Deprecated. Use append_param or append_nuclide")
1811

1812
    def append_param(self, param):
1✔
1813
        """
1814
        Append the parameter to this node.
1815

1816
        .. versionadded:: 1.0.0
1817

1818
            Added to replace ``append``
1819

1820
        :param param: the parameter to add to this node.
1821
        :type param: ParametersNode
1822
        """
1823
        self._nodes.append((param,))
1✔
1824

1825
    def format(self):
1✔
1826
        ret = ""
1✔
1827
        for node in it.chain(*self.nodes):
1✔
1828
            ret += node.format()
1✔
1829
        return ret
1✔
1830

1831
    def __repr__(self):
1✔
1832
        return f"(Materials: {self.nodes})"
1✔
1833

1834
    def _pretty_str(self):
1✔
1835
        INDENT = 2
1✔
1836
        ret = f"<Node: {self.name}: [\n"
1✔
1837
        for val in self.nodes:
1✔
1838
            child_strs = [f"({', '.join([str(v) for v in val])})"]
1✔
1839
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[:-1]])
1✔
1840
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
1841
        ret += " " * INDENT + "]\n"
1✔
1842
        ret += ">"
1✔
1843
        return ret
1✔
1844

1845
    def __iter__(self):
1✔
1846
        return iter(self.nodes)
1✔
1847

1848
    @property
1✔
1849
    def comments(self):
1✔
1850
        for node in self.nodes:
1✔
1851
            for value in node:
1✔
1852
                yield from value.comments
1✔
1853

1854
    def get_trailing_comment(self):
1✔
1855
        tail = self.nodes[-1]
1✔
1856
        tail = tail[-1]
1✔
1857
        return tail.get_trailing_comment()
1✔
1858

1859
    def _delete_trailing_comment(self):
1✔
1860
        tail = self.nodes[-1]
1✔
1861
        tail = tail[-1]
1✔
1862
        tail._delete_trailing_comment()
1✔
1863

1864
    def flatten(self):
1✔
1865
        ret = []
1✔
1866
        for node_group in self.nodes:
1✔
1867
            ret += node_group
1✔
1868
        return ret
1✔
1869

1870

1871
class ShortcutNode(ListNode):
1✔
1872
    """
1873
    A node that pretends to be a :class:`ListNode` but is actually representing a shortcut.
1874

1875
    This takes the shortcut tokens, and expands it into their "virtual" values.
1876

1877
    :param p: the parsing object to parse.
1878
    :type p: sly.yacc.YaccProduction
1879
    :param short_type: the type of the shortcut.
1880
    :type short_type: Shortcuts
1881
    """
1882

1883
    _shortcut_names = {
1✔
1884
        ("REPEAT", "NUM_REPEAT"): Shortcuts.REPEAT,
1885
        ("JUMP", "NUM_JUMP"): Shortcuts.JUMP,
1886
        ("INTERPOLATE", "NUM_INTERPOLATE"): Shortcuts.INTERPOLATE,
1887
        ("LOG_INTERPOLATE", "NUM_LOG_INTERPOLATE"): Shortcuts.LOG_INTERPOLATE,
1888
        ("MULTIPLY", "NUM_MULTIPLY"): Shortcuts.MULTIPLY,
1889
    }
1890
    _num_finder = re.compile(r"\d+")
1✔
1891

1892
    def __init__(self, p=None, short_type=None, data_type=float):
1✔
1893
        self._type = None
1✔
1894
        self._end_pad = None
1✔
1895
        self._nodes = collections.deque()
1✔
1896
        self._original = []
1✔
1897
        self._full = False
1✔
1898
        self._num_node = ValueNode(None, float, never_pad=True)
1✔
1899
        self._data_type = data_type
1✔
1900
        if p is not None:
1✔
1901
            for search_strs, shortcut in self._shortcut_names.items():
1✔
1902
                for search_str in search_strs:
1✔
1903
                    if hasattr(p, search_str):
1✔
1904
                        super().__init__(search_str.lower())
1✔
1905
                        self._type = shortcut
1✔
1906
            if self._type is None:
1✔
1907
                raise ValueError("must use a valid shortcut")
1✔
1908
            self._original = list(p)
1✔
1909
            if self._type == Shortcuts.REPEAT:
1✔
1910
                self._expand_repeat(p)
1✔
1911
            elif self._type == Shortcuts.MULTIPLY:
1✔
1912
                self._expand_multiply(p)
1✔
1913
            elif self._type == Shortcuts.JUMP:
1✔
1914
                self._expand_jump(p)
1✔
1915
            elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
1916
                self._expand_interpolate(p)
1✔
1917
        elif short_type is not None:
1✔
1918
            if not isinstance(short_type, Shortcuts):
1✔
1919
                raise TypeError(f"Shortcut type must be Shortcuts. {short_type} given.")
1✔
1920
            self._type = short_type
1✔
1921
            if self._type in {
1✔
1922
                Shortcuts.INTERPOLATE,
1923
                Shortcuts.LOG_INTERPOLATE,
1924
                Shortcuts.JUMP,
1925
                Shortcuts.JUMP,
1926
            }:
1927
                self._num_node = ValueNode(None, int, never_pad=True)
1✔
1928
            self._end_pad = PaddingNode(" ")
1✔
1929

1930
    def load_nodes(self, nodes):
1✔
1931
        """
1932
        Loads the given nodes into this shortcut, and update needed information.
1933

1934
        For interpolate nodes should start and end with the beginning/end of
1935
        the interpolation.
1936

1937
        :param nodes: the nodes to be loaded.
1938
        :type nodes: list
1939
        """
1940
        self._nodes = collections.deque(nodes)
1✔
1941
        if self.type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
1942
            self._begin = nodes[0].value
1✔
1943
            self._end = nodes[-1].value
1✔
1944
            if self.type == Shortcuts.LOG_INTERPOLATE:
1✔
1945
                self._begin = math.log10(self._begin)
1✔
1946
                self._end = math.log10(self._end)
1✔
1947
            self._spacing = (self._end - self._begin) / (len(nodes) - 1)
1✔
1948

1949
    @property
1✔
1950
    def end_padding(self):
1✔
1951
        """
1952
        The padding at the end of this shortcut.
1953

1954
        :rtype: PaddingNode
1955
        """
1956
        return self._end_pad
1✔
1957

1958
    @end_padding.setter
1✔
1959
    def end_padding(self, padding):
1✔
1960
        if not isinstance(padding, PaddingNode):
1✔
1961
            raise TypeError(
1✔
1962
                f"End padding must be of type PaddingNode. {padding} given."
1963
            )
1964
        self._end_pad = padding
1✔
1965

1966
    @property
1✔
1967
    def type(self):
1✔
1968
        """
1969
        The Type of shortcut this ShortcutNode represents.
1970

1971
        :rtype: Shortcuts
1972
        """
1973
        return self._type
1✔
1974

1975
    def __repr__(self):
1✔
1976
        return f"(shortcut:{self._type}: {self.nodes})"
1✔
1977

1978
    def _get_last_node(self, p):
1✔
1979
        last = p[0]
1✔
1980
        if isinstance(last, ValueNode):
1✔
1981
            return collections.deque([last])
1✔
1982
        return collections.deque()
1✔
1983

1984
    def _expand_repeat(self, p):
1✔
1985
        self._nodes = self._get_last_node(p)
1✔
1986
        repeat = p[1]
1✔
1987
        try:
1✔
1988
            repeat_num_str = repeat.lower().replace("r", "")
1✔
1989
            repeat_num = int(repeat_num_str)
1✔
1990
            self._num_node = ValueNode(repeat_num, int, never_pad=True)
1✔
1991
        except ValueError:
1✔
1992
            repeat_num = 1
1✔
1993
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
1994
        if isinstance(p[0], ValueNode):
1✔
1995
            last_val = p[0]
1✔
1996
        else:
1997
            if isinstance(p[0], GeometryTree):
1✔
1998
                last_val = list(p[0])[-1]
1✔
1999
            else:
2000
                last_val = p[0].nodes[-1]
1✔
2001
        if last_val.value is None:
1✔
2002
            raise ValueError(f"Repeat cannot follow a jump. Given: {list(p)}")
1✔
2003
        self._nodes += [copy.deepcopy(last_val) for i in range(repeat_num)]
1✔
2004

2005
    def _expand_multiply(self, p):
1✔
2006
        self._nodes = self._get_last_node(p)
1✔
2007
        mult_str = p[1].lower().replace("m", "")
1✔
2008
        mult_val = fortran_float(mult_str)
1✔
2009
        self._num_node = ValueNode(mult_str, float, never_pad=True)
1✔
2010
        if isinstance(p[0], ValueNode):
1✔
2011
            last_val = self.nodes[-1]
1✔
2012
        elif isinstance(p[0], GeometryTree):
1✔
2013
            if "right" in p[0].nodes:
1✔
2014
                last_val = p[0].nodes["right"]
1✔
2015
            else:
2016
                last_val = p[0].nodes["left"]
×
2017
        else:
2018
            last_val = p[0].nodes[-1]
1✔
2019
        if last_val.value is None:
1✔
2020
            raise ValueError(f"Multiply cannot follow a jump. Given: {list(p)}")
1✔
2021
        self._nodes.append(copy.deepcopy(last_val))
1✔
2022
        self.nodes[-1].value *= mult_val
1✔
2023

2024
    def _expand_jump(self, p):
1✔
2025
        try:
1✔
2026
            jump_str = p[0].lower().replace("j", "")
1✔
2027
            jump_num = int(jump_str)
1✔
2028
            self._num_node = ValueNode(jump_str, int, never_pad=True)
1✔
2029
        except ValueError:
1✔
2030
            jump_num = 1
1✔
2031
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2032
        for i in range(jump_num):
1✔
2033
            self._nodes.append(ValueNode(input_parser.mcnp_input.Jump(), float))
1✔
2034

2035
    def _expand_interpolate(self, p):
1✔
2036
        if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2037
            is_log = True
1✔
2038
        else:
2039
            is_log = False
1✔
2040
        if hasattr(p, "geometry_term"):
1✔
2041
            term = p.geometry_term
1✔
2042
            if isinstance(term, GeometryTree):
1✔
2043
                begin = list(term)[-1].value
1✔
2044
            else:
2045
                begin = term.value
1✔
2046
            end = p.number_phrase.value
1✔
2047
        else:
2048
            if isinstance(p[0], ListNode):
1✔
2049
                begin = p[0].nodes[-1].value
1✔
2050
            else:
2051
                begin = p[0].value
1✔
2052
            end = p.number_phrase.value
1✔
2053
        self._nodes = self._get_last_node(p)
1✔
2054
        if begin is None:
1✔
2055
            raise ValueError(f"Interpolates cannot follow a jump. Given: {list(p)}")
1✔
2056
        match = self._num_finder.search(p[1])
1✔
2057
        if match:
1✔
2058
            number = int(match.group(0))
1✔
2059
            self._num_node = ValueNode(match.group(0), int, never_pad=True)
1✔
2060
        else:
2061
            number = 1
1✔
2062
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2063
        if is_log:
1✔
2064
            begin = math.log(begin, 10)
1✔
2065
            end = math.log(end, 10)
1✔
2066
        spacing = (end - begin) / (number + 1)
1✔
2067
        for i in range(number):
1✔
2068
            if is_log:
1✔
2069
                new_val = 10 ** (begin + spacing * (i + 1))
1✔
2070
            else:
2071
                new_val = begin + spacing * (i + 1)
1✔
2072
            self.append(
1✔
2073
                ValueNode(
2074
                    str(self._data_type(new_val)), self._data_type, never_pad=True
2075
                )
2076
            )
2077
        self._begin = begin
1✔
2078
        self._end = end
1✔
2079
        self._spacing = spacing
1✔
2080
        self.append(p.number_phrase)
1✔
2081

2082
    def _can_consume_node(self, node, direction, last_edge_shortcut=False):
1✔
2083
        """
2084
        If it's possible to consume this node.
2085

2086
        :param node: the node to consume
2087
        :type node: ValueNode
2088
        :param direction: the direct to go in. Must be in {-1, 1}
2089
        :type direction: int
2090
        :param last_edge_shortcut: Whether the previous node in the list was part of a different shortcut
2091
        :type last_edge_shortcut: bool
2092
        :returns: true it can be consumed.
2093
        :rtype: bool
2094
        """
2095
        if self._type == Shortcuts.JUMP:
1✔
2096
            if node.value is None:
1✔
2097
                return True
1✔
2098

2099
        # REPEAT
2100
        elif self._type == Shortcuts.REPEAT:
1✔
2101
            if len(self.nodes) == 0 and node.value is not None:
1✔
2102
                return True
1✔
2103
            if direction == 1:
1✔
2104
                edge = self.nodes[-1]
1✔
2105
            else:
2106
                edge = self.nodes[0]
1✔
2107
            if edge.type != node.type or edge.value is None or node.value is None:
1✔
2108
                return False
1✔
2109
            if edge.type in {int, float} and math.isclose(
1✔
2110
                edge.value, node.value, rel_tol=rel_tol, abs_tol=abs_tol
2111
            ):
2112
                return True
1✔
2113
            elif edge.value == node.value:
1✔
2114
                return True
×
2115

2116
        # INTERPOLATE
2117
        elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2118
            return self._is_valid_interpolate_edge(node, direction)
1✔
2119
        # Multiply can only ever have 1 value
2120
        elif self._type == Shortcuts.MULTIPLY:
1✔
2121
            # can't do a multiply with a Jump
2122
            if node.value is None:
1✔
2123
                return False
1✔
2124
            if len(self.nodes) == 0:
1✔
2125
                # clear out old state if needed
2126
                self._full = False
1✔
2127
                if last_edge_shortcut:
1✔
2128
                    self._full = True
1✔
2129
                return True
1✔
2130
            if len(self.nodes) == 1 and not self._full:
1✔
2131
                return True
1✔
2132
        return False
1✔
2133

2134
    def _is_valid_interpolate_edge(self, node, direction):
1✔
2135
        """
2136
        Is a valid interpolation edge.
2137

2138
        :param node: the node to consume
2139
        :type node: ValueNode
2140
        :param direction: the direct to go in. Must be in {-1, 1}
2141
        :type direction: int
2142
        :returns: true it can be consumed.
2143
        :rtype: bool
2144
        """
2145
        # kill jumps immediately
2146
        if node.value is None:
1✔
2147
            return False
1✔
2148
        if len(self.nodes) == 0:
1✔
2149
            new_val = self._begin if direction == 1 else self._end
1✔
2150
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2151
                new_val = 10**new_val
1✔
2152
        else:
2153
            edge = self.nodes[-1] if direction == 1 else self.nodes[0]
1✔
2154
            edge = edge.value
1✔
2155
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2156
                edge = math.log(edge, 10)
1✔
2157
                new_val = 10 ** (edge + direction * self._spacing)
1✔
2158
            else:
2159
                new_val = edge + direction * self._spacing
1✔
2160
        return math.isclose(new_val, node.value, rel_tol=rel_tol, abs_tol=abs_tol)
1✔
2161

2162
    def consume_edge_node(self, node, direction, last_edge_shortcut=False):
1✔
2163
        """
2164
        Tries to consume the given edge.
2165

2166
        If it can be consumed the node is appended to the internal nodes.
2167

2168
        :param node: the node to consume
2169
        :type node: ValueNode
2170
        :param direction: the direct to go in. Must be in {-1, 1}
2171
        :type direction: int
2172
        :param last_edge_shortcut: Whether or the previous node in the list was
2173
            part of a different shortcut
2174
        :type last_edge_shortcut: bool
2175
        :returns: True if the node was consumed.
2176
        :rtype: bool
2177
        """
2178
        if self._can_consume_node(node, direction, last_edge_shortcut):
1✔
2179
            if direction == 1:
1✔
2180
                self._nodes.append(node)
1✔
2181
            else:
2182
                self._nodes.appendleft(node)
1✔
2183
            return True
1✔
2184
        return False
1✔
2185

2186
    def format(self, leading_node=None):
1✔
2187
        if self._type == Shortcuts.JUMP:
1✔
2188
            temp = self._format_jump()
1✔
2189
        # repeat
2190
        elif self._type == Shortcuts.REPEAT:
1✔
2191
            temp = self._format_repeat(leading_node)
1✔
2192
        # multiply
2193
        elif self._type == Shortcuts.MULTIPLY:
1✔
2194
            temp = self._format_multiply(leading_node)
1✔
2195
        # interpolate
2196
        elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2197
            temp = self._format_interpolate(leading_node)
1✔
2198
        if self.end_padding:
1✔
2199
            pad_str = self.end_padding.format()
1✔
2200
        else:
2201
            pad_str = ""
1✔
2202
        return f"{temp}{pad_str}"
1✔
2203

2204
    def _format_jump(self):
1✔
2205
        num_jumps = len(self.nodes)
1✔
2206
        if num_jumps == 0:
1✔
2207
            return ""
×
2208
        if len(self._original) > 0 and "j" in self._original[0]:
1✔
2209
            j = "j"
1✔
2210
        else:
2211
            j = "J"
1✔
2212
        length = len(self._original)
1✔
2213
        self._num_node.value = num_jumps
1✔
2214
        if num_jumps == 1 and (
1✔
2215
            length == 0 or (length > 0 and "1" not in self._original[0])
2216
        ):
2217
            num_jumps = ""
1✔
2218
        else:
2219
            num_jumps = self._num_node
1✔
2220

2221
        return f"{num_jumps.format()}{j}"
1✔
2222

2223
    def _can_use_last_node(self, node, start=None):
1✔
2224
        """
2225
        Determine if the previous node can be used as the start to this node
2226
        (and therefore skip the start of this one).
2227

2228
        Last node can be used if
2229
        - it's a basic ValueNode that matches this repeat
2230
        - it's also a shortcut, with the same edge values.
2231

2232
        :param node: the previous node to test.
2233
        :type node: ValueNode, ShortcutNode
2234
        :param start: the starting value for this node (specifically for interpolation)
2235
        :type start: float
2236
        :returns: True if the node given can be used.
2237
        :rtype: bool
2238
        """
2239
        if isinstance(node, ValueNode):
1✔
2240
            value = node.value
×
2241
        elif isinstance(node, ShortcutNode):
1✔
2242
            value = node.nodes[-1].value
1✔
2243
        else:
2244
            return False
1✔
2245
        if value is None:
1✔
2246
            return False
1✔
2247
        if start is None:
1✔
2248
            start = self.nodes[0].value
1✔
2249
        return math.isclose(start, value)
1✔
2250

2251
    def _format_repeat(self, leading_node=None):
1✔
2252

2253
        if self._can_use_last_node(leading_node):
1✔
2254
            first_val = ""
1✔
2255
            num_extra = 0
1✔
2256
        else:
2257
            first_val = self.nodes[0].format()
1✔
2258
            num_extra = 1
1✔
2259
        num_repeats = len(self.nodes) - num_extra
1✔
2260
        self._num_node.value = num_repeats
1✔
2261
        if len(self._original) >= 2 and "r" in self._original[1]:
1✔
2262
            r = "r"
1✔
2263
        else:
2264
            r = "R"
1✔
2265
        if (
1✔
2266
            num_repeats == 1
2267
            and len(self._original) >= 2
2268
            and "1" not in self._original[1]
2269
        ):
2270
            num_repeats = ""
1✔
2271
        else:
2272
            num_repeats = self._num_node
1✔
2273
        return f"{first_val}{num_repeats.format()}{r}"
1✔
2274

2275
    def _format_multiply(self, leading_node=None):
1✔
2276
        # Multiply doesn't usually consume other nodes
2277
        if leading_node is not None and len(self) == 1:
1✔
2278
            first_val = leading_node.nodes[-1]
1✔
2279
            first_val_str = ""
1✔
2280
        else:
2281
            first_val = self.nodes[0]
1✔
2282
            first_val_str = first_val
1✔
2283
        if self._original and "m" in self._original[-1]:
1✔
2284
            m = "m"
1✔
2285
        else:
2286
            m = "M"
1✔
2287
        self._num_node.value = self.nodes[-1].value / first_val.value
1✔
2288
        return f"{first_val_str.format()}{self._num_node.format()}{m}"
1✔
2289

2290
    def _format_interpolate(self, leading_node=None):
1✔
2291
        begin = self._begin
1✔
2292
        if self.type == Shortcuts.LOG_INTERPOLATE:
1✔
2293
            begin = 10**begin
1✔
2294
        if self._can_use_last_node(leading_node, begin):
1✔
2295
            start = ""
1✔
2296
            num_extra_nodes = 1
1✔
2297
            if hasattr(self, "_has_pseudo_start"):
1✔
2298
                num_extra_nodes += 1
1✔
2299
        else:
2300
            start = self.nodes[0]
1✔
2301
            num_extra_nodes = 2
1✔
2302
        end = self.nodes[-1]
1✔
2303
        num_interp = len(self.nodes) - num_extra_nodes
1✔
2304
        self._num_node.value = num_interp
1✔
2305
        interp = "I"
1✔
2306
        can_match = False
1✔
2307
        if len(self._original) > 0:
1✔
2308
            if match := re.match(r"\d*(\w+)", self._original[1]):
1✔
2309
                can_match = True
1✔
2310
                interp = match.group(1)
1✔
2311
        if not can_match:
1✔
2312
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2313
                interp = "ILOG"
1✔
2314
        if (
1✔
2315
            num_interp == 1
2316
            and len(self._original) >= 2
2317
            and "1" not in self._original[1]
2318
        ):
2319
            num_interp = ""
1✔
2320
        else:
2321
            num_interp = self._num_node
1✔
2322
        if len(self._original) >= 3:
1✔
2323
            padding = self._original[2]
1✔
2324
        else:
2325
            padding = PaddingNode(" ")
1✔
2326
        return f"{start.format()}{num_interp.format()}{interp}{padding.format()}{end.format()}"
1✔
2327

2328

2329
class ClassifierNode(SyntaxNodeBase):
1✔
2330
    """
2331
    A node to represent the classifier for a :class:`montepy.data_input.DataInput`
2332

2333
    e.g., represents ``M4``, ``F104:n,p``, ``IMP:n,e``.
2334
    """
2335

2336
    def __init__(self):
1✔
2337
        super().__init__("classifier")
1✔
2338
        self._prefix = None
1✔
2339
        self._number = None
1✔
2340
        self._particles = None
1✔
2341
        self._modifier = None
1✔
2342
        self._padding = None
1✔
2343
        self._nodes = []
1✔
2344

2345
    @property
1✔
2346
    def prefix(self):
1✔
2347
        """
2348
        The prefix for the classifier.
2349

2350
        That is the string that tells what type of input this is.
2351

2352
        E.g.: ``M`` in ``M4`` or ``IMP`` in ``IMP:n``.
2353

2354
        :returns: the prefix
2355
        :rtype: ValueNode
2356
        """
2357
        return self._prefix
1✔
2358

2359
    @prefix.setter
1✔
2360
    def prefix(self, pref):
1✔
2361
        self.append(pref)
1✔
2362
        self._prefix = pref
1✔
2363

2364
    @property
1✔
2365
    def number(self):
1✔
2366
        """
2367
        The number if any for the classifier.
2368

2369
        :returns: the number holder for this classifier.
2370
        :rtype: ValueNode
2371
        """
2372
        return self._number
1✔
2373

2374
    @number.setter
1✔
2375
    def number(self, number):
1✔
2376
        self.append(number)
1✔
2377
        self._number = number
1✔
2378

2379
    @property
1✔
2380
    def particles(self):
1✔
2381
        """
2382
        The particles if any tied to this classifier.
2383

2384
        :returns: the particles used.
2385
        :rtype: ParticleNode
2386
        """
2387
        return self._particles
1✔
2388

2389
    @particles.setter
1✔
2390
    def particles(self, part):
1✔
2391
        self.append(part)
1✔
2392
        self._particles = part
1✔
2393

2394
    @property
1✔
2395
    def modifier(self):
1✔
2396
        """
2397
        The modifier for this classifier if any.
2398

2399
        A modifier is a prefix character that changes the inputs behavior,
2400
        e.g.: ``*`` or ``+``.
2401

2402
        :returns: the modifier
2403
        :rtype: ValueNode
2404
        """
2405
        return self._modifier
1✔
2406

2407
    @modifier.setter
1✔
2408
    def modifier(self, mod):
1✔
2409
        self.append(mod)
1✔
2410
        self._modifier = mod
1✔
2411

2412
    @property
1✔
2413
    def padding(self):
1✔
2414
        """
2415
        The padding for this classifier.
2416

2417
        .. Note::
2418
            None of the ValueNodes in this object should have padding.
2419

2420
        :returns: the padding after the classifier.
2421
        :rtype: PaddingNode
2422
        """
2423
        return self._padding
1✔
2424

2425
    @padding.setter
1✔
2426
    def padding(self, val):
1✔
2427
        self.append(val)
1✔
2428
        self._padding = val
1✔
2429

2430
    def format(self):
1✔
2431
        if self.modifier:
1✔
2432
            ret = self.modifier.format()
1✔
2433
        else:
2434
            ret = ""
1✔
2435
        ret += self.prefix.format()
1✔
2436
        if self.number:
1✔
2437
            ret += self.number.format()
1✔
2438
        if self.particles:
1✔
2439
            ret += self.particles.format()
1✔
2440
        if self.padding:
1✔
2441
            ret += self.padding.format()
1✔
2442
        return ret
1✔
2443

2444
    def __str__(self):
1✔
2445
        return self.format()
1✔
2446

2447
    def __repr__(self):
1✔
2448
        return (
1✔
2449
            f"(Classifier: mod: {self.modifier}, prefix: {self.prefix}, "
2450
            f"number: {self.number}, particles: {self.particles},"
2451
            f" padding: {self.padding})"
2452
        )
2453

2454
    def _pretty_str(self):
1✔
2455
        return f"""<Classifier: {{ 
1✔
2456
    mod: {self.modifier}, 
2457
    prefix: {self.prefix}, 
2458
    number: {self.number}, 
2459
    particles: {self.particles},
2460
    padding: {self.padding} 
2461
  }}
2462
>
2463
"""
2464

2465
    @property
1✔
2466
    def comments(self):
1✔
2467
        if self.padding is not None:
1✔
2468
            yield from self.padding.comments
1✔
2469
        else:
2470
            yield from []
1✔
2471

2472
    def get_trailing_comment(self):
1✔
2473
        if self.padding:
1✔
2474
            return self.padding.get_trailing_comment()
1✔
2475

2476
    def _delete_trailing_comment(self):
1✔
2477
        if self.padding:
1✔
2478
            self.padding._delete_trailing_comment()
1✔
2479

2480
    def flatten(self):
1✔
2481
        ret = []
1✔
2482
        if self.modifier:
1✔
2483
            ret.append(self.modifier)
1✔
2484
        ret.append(self.prefix)
1✔
2485
        if self.number:
1✔
2486
            ret.append(self.number)
1✔
2487
        if self.particles:
1✔
2488
            ret.append(self.particles)
1✔
2489
        if self.padding:
1✔
2490
            ret.append(self.padding)
1✔
2491
        return ret
1✔
2492

2493

2494
class ParametersNode(SyntaxNodeBase):
1✔
2495
    """
2496
    A node to hold the parameters, key-value pairs, for this input.
2497

2498
    This behaves like a dictionary and is accessible by their key*
2499

2500
    .. Note::
2501
        How to access values.
2502

2503
        The internal dictionary doesn't use the full classifier directly,
2504
        because some parameters should not be both allowed: e.g., ``fill`` and ``*fill``.
2505
        The key is a string that is all lower case, and only uses the classifiers prefix,
2506
        and particles.
2507

2508
        So to access a cell's fill information you would run:
2509

2510
        .. code-block:: python
2511

2512
            parameters["fill"]
2513

2514
        And to access the n,p importance:
2515

2516
        .. code-block:: python
2517

2518
            parameters["imp:n,p"]
2519
    """
2520

2521
    def __init__(self):
1✔
2522
        super().__init__("parameters")
1✔
2523
        self._nodes = {}
1✔
2524

2525
    def append(self, val, is_default=False):
1✔
2526
        """
2527
        Append the node to this node.
2528

2529
        This takes a syntax node, which requires the keys:
2530
            ``["classifier", "seperator", "data"]``
2531

2532
        :param val: the parameter to append.
2533
        :type val: SyntaxNode
2534
        :param is_default: whether this parameter was added as a default tree not from the user.
2535
        :type is_default: bool
2536
        """
2537
        classifier = val["classifier"]
1✔
2538
        key = (
1✔
2539
            classifier.prefix.value
2540
            + (str(classifier.particles) if classifier.particles else "")
2541
        ).lower()
2542
        if key in self._nodes:
1✔
2543
            raise RedundantParameterSpecification(key, val)
1✔
2544
        if is_default:
1✔
2545
            val._is_default = True
1✔
2546
        self._nodes[key] = val
1✔
2547

2548
    def __str__(self):
1✔
2549
        return f"<Parameters, {self.nodes}>"
1✔
2550

2551
    def __repr__(self):
1✔
2552
        return str(self)
1✔
2553

2554
    def _pretty_str(self):
1✔
2555
        INDENT = 2
1✔
2556
        ret = f"<Node: {self.name}: {{\n"
1✔
2557
        for key, val in self.nodes.items():
1✔
2558
            child_strs = val._pretty_str().split("\n")
1✔
2559
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
2560
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
2561
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
2562
        ret += " " * INDENT + "}\n"
1✔
2563
        ret += ">"
1✔
2564
        return ret
1✔
2565

2566
    def __getitem__(self, key):
1✔
2567
        return self.nodes[key.lower()]
1✔
2568

2569
    def __contains__(self, key):
1✔
2570
        return key.lower() in self.nodes
1✔
2571

2572
    def format(self):
1✔
2573
        ret = ""
1✔
2574
        for node in self.nodes.values():
1✔
2575
            ret += node.format()
1✔
2576
        return ret
1✔
2577

2578
    def get_trailing_comment(self):
1✔
2579
        for node in reversed(self.nodes.values()):
1✔
2580
            if hasattr(node, "_is_default"):
1✔
2581
                continue
1✔
2582
            return node.get_trailing_comment()
1✔
2583

2584
    def _delete_trailing_comment(self):
1✔
2585
        for node in reversed(self.nodes.values()):
1✔
2586
            if hasattr(node, "_is_default"):
1✔
2587
                continue
1✔
2588
            node._delete_trailing_comment()
1✔
2589

2590
    @property
1✔
2591
    def comments(self):
1✔
2592
        for node in self.nodes.values():
1✔
2593
            if isinstance(node, SyntaxNodeBase):
1✔
2594
                yield from node.comments
1✔
2595

2596
    def flatten(self):
1✔
2597
        ret = []
1✔
2598
        for node in self.nodes.values():
1✔
2599
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
2600
                ret.append(node)
×
2601
            else:
2602
                ret += node.flatten()
1✔
2603
        return ret
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

© 2026 Coveralls, Inc