Developer’s Guide#
MontePy can be thought of as having two layers: the syntax, and the semantic layers. The syntax layers handle the boring syntax things: like multi-line cards, and comments, etc. The semantic layer takes this information and makes sense of it, like what the material number in a cell card is.
Note
Punchcards are dead. For this reason MontePy refrains from using antiquated terminology like “cards” and “decks”. Instead MontePy refers to “inputs”, and “files” or “problems”.
Note
Demo code is based on tests/inputs/test.imcnp. You can load this with:
import montepy problem = montepy.read_input("tests/inputs/test.imcnp")
Contributing#
Here is a getting started guide to contributing. If you have any questions Micah and Travis are available to give input and answer your questions. Before contributing you should review the MontePy Scope and design philosophy.
Setting up and Typical Development Workflow#
Clone the repository.
Install the required packages. MontePy comes with the requirements specfied in
pyproject.toml
. Optional packages are also specified. To install all packages needed for development simply run:pip install .[develop]
Tie your work to an issue. All work on MontePy is tracked through issues. If you are working on a new feature or bug that is not covered by an issue, please file an issue first.
Work on a new branch. The branches:
develop
andmain
are protected. All new code must be accepted through a merge request or pull request. The easiest way to make this branch is to “create pull request” from github. This will create a new branch (though with an unwieldy name) that you can checkout and work on.Run the test cases. MontePy relies heavily on its over 380 tests for the development process. These are configured so if you run:
pytest
from the root of the git repository all tests will be found and ran.Develop test cases. This is especially important if you are working on a bug fix. A merge request will not be accepted until it can be shown that a test case can replicate the bug and does in deed fail without the bug fix in place. To achieve this, it is recommended that you commit the test first, and push it to gitlab. This way there will be a record of the CI pipeline failing that can be quickly reviewed as part of the merge request.
MontePy is currently working on migrating from
unittest
topytest
for test fixtures. All new tests should use apytest
architecture. Generally unit tests of new features go in the test file with the closest class name. Integration tests have all been dumped intests/test_integration.py
. For integration tests you can likely use thetests/inputs/test.imcnp
input file. This is pre-loaded as anMCNP_Problem
stored as:self.simple_problem
. If you need to mutate it at all you must first make acopy.deepcopy
of it.Write the code.
Document all new classes and functions. MontePy uses Sphinx docstrings.
Format the code with
black
. You can simply runblack montepy tests
Add more test cases as necessary. The merge request should show you the code coverage. The general goal is near 100% coverage.
Update the documentation. Read the “Getting Started” guide and the “Developer’s Guide”, and see if any information there should be updated. If you expect the feature to be commonly used it should be mentioned in the getting started guide. Otherwise just the docstrings may suffice. Another option is to write an example in the “Tips and Tricks” guide.
Update the authors as necessary. The authors information is in
AUTHORS
andpyproject.toml
.Start a merge request review. Generally Micah (@micahgale) or Travis (@tjlaboss) are good reviewers.
Deploy Process#
MontePy currently does not use a continuous deploy (CD) process.
Changes are staged on the develop
branch prior to a release.
Both develop
and main
are protected branches.
main
is only be used for releases.
If someone clones main
they will get the most recent official release.
Only a select few core-developers are allowed to approve a merge to main
and therefore a new release.
develop
is for production quality code that has been approved for release,
but is waiting on the next release.
So all new features and bug fixes must first be merged onto develop
.
The expectation is that features once merged onto develop
are stable,
well tested, well documented, and well-formatted.
Versioning#
Version information is stored in git tags, and retrieved using setuptools scm. The version tag shall match the regular expression:
v\d\.\d+\.\d+
.
These tags will be applied by a maintainer during the release process, and cannot be applied by normal users.
MontePy follows the semantic versioning standard to the best of our abilities.
Additional References:
Merge Checklist#
Here are some common issues to check before approving a merge request.
If this is a bug fix did the new testing fail without the fix?
Were the authors and credits properly updated?
Check also the authors in
pyproject.toml
Is this merge request tied to an issue?
Deploy Checklist#
For a deployment you need to:
Run the deploy script :
.github/scripts/deploy.sh
Manually merge onto main without creating a new commit. This is necessary because there’s no way to do a github PR that will not create a new commit, which will break setuptools_scm.
Update the release notes on the draft release, and finalize it on GitHub.
Package Structure#
Top Level#
The top level of the package is reserved for only a select few objects.
All children of NumberedObjectCollection
can live here.
The other allowed classes are: Exceptions
, MCNP_Card
, MCNP_Problem
, Cell
,
Particle
, and Universe
.
Utility functions are allowed at this level as well.
input_parser#
The montepy.input_parser
contains all functions and classes involved in syntax parsing.
Generally this is all invoked through read_input()
,
which returns an MCNP_Problem
instance.
data_inputs#
This package is for all MCNP_Card
children that should exist
in the data block in an MCNP input.
For example Material
lives here.
surfaces#
This package contains all surface classes.
All classes need to be children of Surface
.
When possible new surface classes should combine similar planes.
For example AxisPlane
covers PX
, PY
, and PZ
.
Introduction to SLY and Syntax Trees#
In MontePy 0.2.0 the core of MontePy was radically changed. A real syntax parser was actually used that actually does things like work with a Lexer, and an L-R table. This parsing engine is SLY (Sly Lex-Yacc). The parsers used by MontePy are designed to return “syntax trees”. These are based on Abstract Syntax Tree, but are not true sytax trees per se. These trees are not abstract. The white-space, and comment information is preserved.
Example Syntax Tree#
Let’s look at a typical cell definition:
1 10 -5.0 1 -2 -3 IMP:N=1 Vol 5.0
This can be broken into large chunks by their type of information
High-level |
|||||
---|---|---|---|---|---|
Cell Number |
Material Definition |
Geometry Definition |
Parameters |
||
|
|
|
|
||
Cell Number |
Material Number |
Density |
Geometry Definition |
Importance |
Volume |
|
|
|
|
|
|
This example shows the first-and-a-half levels of the syntax tree for a Cell. This structure does break down a bit further.
Geometry Example#
For geometry this syntax tree is a binary tree as well and applies the grouping rules properly to build the correct logic into the tree. For instance the previous example’s geometry:
1 -2 -3
Would become:
Geometry
/ \
/ \
1 & / \
/ & \
-2 -3
Introduction To Data Types#
A syntax tree consists of a series of instances of various node objects.
All node classes are sub-classes of the montepy.input_parser.syntax_node.SyntaxNodeBase
class.
The classes are:
SyntaxNode
is one of the most commonly used class, and represents a syntax tree. This is basically a wrapper for a dict (which will be ordered thanks to python 3.8).ValueNode
is the most commonly used classes. It represents the leaves of the syntax tree. It is meant to hold a single value, both its semantic value and its text representation, and its surrounding white-space (and comments), or padding.PaddingNode
is the companion to theValueNode
. It encapsulates all following padding for a value. Padding is considered to be white-space or a comment (CommentNode
).ListNode
is a node meant to contain a list of arbitrary length of values.ShortcutNode
is a helper to aListNode
for when MCNP shortcuts (e.g.,1 10r
) are used. They are nested inside of aListNode
and should be mostly transparent to the user and developer.ParametersNode
is a node to hold the parameters for an input. The parameters are the key-value pairs that can come at the end of most inputs.GeometryTree
is a node for holding the binary trees for the CSG set logic for a cell’s geometry definition. It is the most recursive data structure of any of these nodes.ClassifierNode
is a node to represent the data classification “word” that describes what the data are for. For example for a material it would containM34
. For a cell importance it could beimp:n
. It can contain: a data keyword, a number, a particle designator (ParticleNode
), and a modifier character (e.g.,*
in*TR5
).IsotopesNode
is a node that represents an MCNP style isotope identifier (e.g.,1001.80c
).
Many of these nodes (which aren’t leaves) behave like dicts and lists, and can be accessed with indices. For more detail in how to work with them read the next section on MCNP_Objects: Input: MCNP_Object.
Inheritance#
There are many abstract or simply parent classes that are designed to be subclassed extensively.
Input: MCNP_Object
#
All classes that represent a single input card must subclass this.
For example: some children are: Cell
, Surface
.
How to __init__#
Your init function signature should be: def __init__(self, input)
.
You should then immediately populate default values, and then
call super().__init__(input, self._parser)
.
This way if super().__init__
fails,
there will be enough information for the error reporting to not fail,
when trying to convert the objects to strings.
This will then populate the parameters: _tree
, and comments
.
Now you should (inside an in if block checking input_card
) parse
self._tree
.
Classes need to support “from scratch” creation e.g., cell = Cell()
.
Working with Parsers, and the Syntax Tree#
The parent class init function requires an instance of a parser object.
Note this is an instance, and not the class itself.
The init function will then run parser.parse()
.
Most objects in MontePy will initialize and keep the parser object at the (MontePy) class level, to reduce overhead.
class Cell(MCNP_Object):
# Snip
_parser = CellParser()
# snip
If the input was parsed correctly the syntax tree returned will be stored in self._tree
.
If not the errors will be raised automatically.
The top of the tree will always be an instance of SyntaxNode
.
This will behave like a dictionary, and can be acessed by their keys:
self._number = self._tree["cell_number"]
Almost all leaves on the trees will be instances of ValueNode
.
This has many support functions that you should not try to implement yourself.
The actual semantic values are stored in node.value
, for instance the float value for a float ValueNode.
This property can be set, and should be.
You should not store the nested value; instead you should store the entire ValueNode in a private attribute,
and then use make_prop_val_node()
to provide the appropriate property.
Even if an input isn’t provided a ValueNode needs to be stored. The utility _generate_default_node()
can help simplify this.
The parsers can’t always know what data type should in a specific position, so largely it treats all numerical values as floats.
This should be changed during the init so the value_nodes are the correct data type.
First: if the sign of the value (positive/negative) carries information beyond the value being negative, this should be marked.
For instance, on a cell the density can be positive or negative depending on if it’s atom or mass density.
This doesn’t mean the density is negative.
To mark this set the is_negatable_float()
to True
for floats,
and is_negatable_identifier()
for integers.
This will make it so that value
always returns a positive value, and so is_negative()
returns a boolean value.
Note
Setting is_negatable_identifier()
to True
will convert the ValueNode to an integer ValueNode (via _convert_to_int()
).
Next, if you do not need to change the type()
for the ValueNode, but do not need to markt the ValueNode as negative;
there are methods to handle this.
These methods are _convert_to_int()
, and
_convert_to_enum()
.
_convert_to_int
is a rather straight forward function to run, and takes no arguments.
It should be noted that the value is found by running int(self.token)
, that is that the original string value, and not the float value is converted.
This is in order to avoid allowing 1.5
as a valid int, since in this case the floor would be taken.
_convert_to_enum
takes a class instance, which is a subclass of Enum
.
You can specify a format_type
, which specifies what the data should be treated as while formatting it with new data.
For example SurfaceType
(e.g., PZ
) uses str
as its format type,
whereas Lattice
(e.g., 1
or 2
) uses int
is its format type.
How to __str__ vs __repr__#
All objects must implement __str__
(called by str()
),
and __repr__
(called by repr()
).
See this issue for a more detailed discussion.
In general __str__
should return a one line string with enough information to uniquely identify the object.
For numbered objects this should include their number, and a few high level details.
For __repr__
this should include debugging information.
This should include most if not all internal state information.
See this example for Cell
>>> cell = problem.cells[2]
>>> print(str(cell))
CELL: 2, mat: 2, DENS: 8.0 atom/b-cm
>>> print(repr(cell))
CELL: 2
MATERIAL: 2, ['iron']
density: 8.0 atom/b-cm
SURFACE: 1005, RCC
SURFACE: 1015, CZ
SURFACE: 1020, PZ
SURFACE: 1025, PZ
Writing to File (Format for MCNP Input)#
MontePy (via write_problem()
) writes
a class to file path or file handle by calling its format_for_mcnp_input()
method.
This must return a list of strings that faithfully represent this objects state, and tries to replicate the user formatting.
Each string in the list represents one line in the MCNP input file to be written.
For most cases the default implementation should work great. This is its implementation:
def format_for_mcnp_input(self, mcnp_version):
self.validate()
self._update_values()
return self.wrap_string_for_mcnp(self._tree.format(), mcnp_version, True)
The first call is to validate()
, which is meant to check for illegal states
caused by partially created objects the user hasn’t completed yet.
Next the abstract method, _update_values()
is called.
This function updates the syntax tree with current values.
Most values should not need to be updated, since their value is linked to a ValueNode, which is pointed to and modified by the object.
This should only really by used to update information controlled by other objects.
For instance Cell
will update its material number based on self.material.number
,
since the cell object does not control a material’s number.
Finally self._tree
is formatted.
Remember self._tree
is a syntax tree of type SyntaxNode
.
format()
will create a string based on the syntax tree,
which is updated with the new values that have been provided.
The ValueNode’s implementation does most of the heavy lifting here with reverse engineering the user value,
and then replicating that formatting with the new value.
Collection: NumberedObjectCollection
#
This should be subclassed for any collection of objects that are numbered.
For example: cells, surfaces, materials, universes, tallies, etc.
By default you need to do almost nothing.
The class that will be added to this collection must have the property obj.number
.
How to __init__#
Your init signature should be def __init__(self, objects=None)
All you need to then do is call super,
with the class this will wrap.
For example the init function for Cells
def __init__(self, cells=None):
super().__init__(montepy.Cell, cells)
Collection: NumberedDataObjectCollection
#
This is a subclass of NumberedObjectCollection
,
which is designed for DataInputAbstract
instances.
It is a wrapper that will ensure that all of its items are also in data_inputs()
.
Numbered Object Numbered_MCNP_Object
#
MCNP allows many types of number objects like cells, surfaces, and tallies.
The only thing special about this is that it requires there be the properties:
number
and old_number
.
The old_number
is what was read from the input file, and should not mutate.
The number
is the object’s current number and should mutate.
The parent class provides a system to link to a problem via self._problem
.
Note this field can be None
.
When setting a number you must check for numbering collisions with the method:
check_number()
.
This function returns nothing, but will raise an error when a number collision occurs.
For example the Surface
number setter looks like:
@number.setter
def number(self, number):
assert isinstance(number, int)
assert number > 0
if self._problem:
self._problem.surfaces.check_number(number)
self._surface_number = number
Surface: Surface
#
This is the parent class for all Surface classes.
You will also need to update surface_builder()
.
You should expose clear parameters such as radius
or location
.
format_for_mcnp_input()
is handled by default.
How to __init__#
After running the super init method
you will then have access to self.surface_type
, and self.surface_constants
.
You then need to verify that the surface type is correct, and there are the correct number of surface constants.
You will also need to add a branch in the logic for montepy.surfaces.surface_builder.surface_builder()
.
find_duplicate_surfaces()
#
This function is meant to find very similar surfaces that cause geometry errors,
such as two PZ
surfaces that are 1 micron apart.
This should return a list of surfaces that are within the provided tolerance similar to this one.
Things to consider.
The list provided will not include
self
,self
is not considered redundant with regards toself
.Surfaces can be modified in many ways including: being periodic with respect to a surface, being transformed, being a periodic surface, and being a white surface. To say that two surfaces are duplicate all of these factors must be considered.
Data Inputs: DataInputAbstract
#
This class is the parent for all inputs that show up in the data block.
When adding a child you will also need to update the
parse_data()
function.
This can be done by adding the class to PREFIX_MATCHES
.
In general first comply with standards for this class’s parent: MCNP_Object
.
In addition you will need to implement update_pointers()
if you need it.
During init the inputs’ “name word” (e.g., M3
, kcode
, f7:n
) is validated and parsed.
Conceptually these names can contain up to four sections.
This information is stored in an instance of ClassifierNode
.
A
prefix_modifier
this modifies the whole card with a special character such as*tr5
A
Prefix
, which is a series of letters that identifies the type such asm
A
number
, which numbers it. These must be an unsigned integer.A particle classifier such as
:n,p
.
You control the parsing behavior through three methods: _class_prefix()
,
_has_number()
,
and _has_classifier()
.
See the documentation for how to set these.
Using the parse_data()
function:#
The function parse_data()
handles converting a data_input
to the correct class automatically.
It uses the set PREFIX_MATCH
to do this.
This lists all classes that the function will look into for a matching class prefix.
The parse_data
function will use the fast_parse
option for parsing the data_input.
This method will only match the first word/classifier using the ClassifierParser
.
Based upon this the function will decide which class to run for a full parse.
By default all subclasses will use the DataParser
class.
If you need to use a custom parser you do so by setting self._parser
.
How to add an object to MCNP_Problem
#
the MCNP_Problem
automatically consumes problem level data inputs,
and adds them to itself.
Cards this would be appropriate for would be things like mode
and kcode
.
To do this it uses the dictionary inputs_to_property
in the __load_data_inputs_to_object
method.
To add a problem level data Object you need to
Add it
inputs_to_property
. The key will be the object class, and the value will be a string for the attribute it should be loaded to.Add a property that exposes this attribute in a desirable way.
Making a numbered Object Numbered_MCNP_Object
#
MCNP allows many types of number objects like cells, surfaces, and tallies.
First you need to provide the property number
, and old_number
.
The parent class provides a system to link to a problem via self._problem
.
Note this field can be None
.
When setting a number you must check for numbering collisions with the method:
check_number()
.
This function returns nothing, but will raise an error when a number collision occurs.
For example the Surface
number setter looks like:
@number.setter
def number(self, number):
assert isinstance(number, int)
assert number > 0
if self._problem:
self._problem.surfaces.check_number(number)
self._surface_number = number
Data Cards that Modify Cells CellModifierInput
#
This is a subclass of DataInputAbstract
that is meant to handle data inputs that specify information about,
and modify cells.
For example IMP
changes the importance of a cell and VOL
specifies its volume.
Both of these are appropriate uses of this class.
This class adds a lot of machinery to handle the complexities of these data inputs, that is because these data can be specified in the Cell or Data block.
How to __init__#
Similar to other inputs you need to match the parent signature and run super on it:
def __init__(self, input=None, in_cell_block=False, key=None, value=None):
super().__init__(input, in_cell_block, key, value)
The added arguments add more information for invoking this from a Cell
.
When doing so the in_cell_block
will obviously be true,
and the key
, and value
will be taken from the parameters
syntax tree.
These will all be automatically called from Cell
as discussed below.
Most of the boiler plate will be handled by super.
The goals for init function should be:
initialize default values needed for when this is initialized from a blank call.
Parse the data provided in the
input
, whenin_cell_block
is False.Parse the data given in
key
andvalue
whenin_cell_block
is True.
On data Ownership#
Objects that subclass this one will only be owned by Cell
and Cells
objects.
They will only be public properties for Cell
.
All “data” must be only in the Cell
level object once the problem has been fully initialized.
This means that the object owned by Cells
should not know the importance of an individual cell,
only the object owned by Cell
should know this.
The general rule is that the Cell
level the object (or some part of it) should be available as a public property.
At the Cells
level the object should be stored in a _protected
attribute.
See more below.
How these objects are added to Cell
and Cells
#
Due to the number of classes that will ultimately be subclasses of this class,
some automated hooks have been developed.
These hooks use a dictionary and the setattr
function to add multiple objects
to Cell
or Cells
automatically.
On the Cell level the static dictionary: Cell._INPUTS_TO_PROPERTY
maps how data should be
loaded.
The key is the class of the object type that should be loaded.
The value is then a tuple.
The first element of the tuple is the string of the attribute to where the object of this class should be loaded.
The second element of the tuple is a boolean.
If this boolean is false repeats of this class are allowed and they will be merged.
(e.g., IMP:N,P=1 IMP:E=0
makes sense despite there being two IMP
specified.
If True only one instance of the object is allowed.
(e.g., VOL=5 VOL=10
makes no sense).
For finding which class to use the PREFIX_MATCHES()
set is used. See above.
The key, value pairs in Cell.parameters
is iterated over.
If any of the keys is a partial match to the PREFIX_MATCHES
dict then that class is used,
and constructed.
The new object is then loaded into the Cell
object at the given attribute using setattr
.
If your class is properly specified in both dictionaries you should be good to go on the Cell
level.
Finally, for objects that are default, and contain no information, a default syntax tree is loaded into the parent Cell
’s syntax tree.
At the Cells
level the same dictionary (Cell._INPUTS_TO_PROPERTY
) is used as well.
This time though it is iterating over problem.data_inputs
.
Thanks to data_parser
these objects are already appropriately typed,
and the corresponding object just needs to be loaded into an attribute.
Once again none of these attributes should be exposed through @property
at the Cells
level.
format_for_mcnp_input()
#
For the most part the complexity of switching between the cell and data block printing is automatically handled by this parent function.
In general this looks a lot like the workflow for the base format_for_mcnp_input
implementation.
However, must internal calls are wrapped in another function, allowing overriding of those wrappers to change behavior for more complex situations.
In all cases _is_worth_printing()
is checked to see if there is information to be printed.
The default implementation checks has_information()
for either the cell or cells.
Next the values need to be updated via _update_values()
.
For the cell level instance this calls _update_cell_values()
,
which needs to be implemented.
For the data-block isntance this is a bit more complicated.
First all new data for every cell is collected by _collect_new_values()
.
By default this will get the ValueNode that is returned from the abstract method _tree_value()
.
These values will then be passed to update_with_new_values()
.
Finally, the syntax tree is formatted.
Once again this is wrapped to allow adding more complexity.
The tree is formatted by _format_tree()
.
merge()
#
This abstract method allows multiple objects of the same type to be combined,
and one will be consumed by the other.
One use case for this is combining the data from: IMP:N,P=1 IMP:E=0.5
into one object
so there’s no redundant data.
This will automatically be called by the loading hooks, and you do not need to worry about
deleting other.
If merging isn’t allowed MalformedInputError
should be raised.
push_to_cells()
#
This is how data provided in the data block are provided to the Cell
objects.
There should be a self.in_cell_block
guard.
You need to check that there was no double specifying of data in both the cell and data block.
This should be raise MalformedInputError
.
This checking and error handling is handled by the method _check_redundant_definitions()
.
_clear_data()
#
This method will get called on data block instances. The goal is to delete any internal data that has already been pushed to the cells so that if a user goes crazy and somehow access this object they cannot modify the data, and get into weird end-use behavior.
print_in_data_block()
#
There is a flag system for controlling if data are output in the cell block or the data block.
This is controlled by print_in_data_block()
.
This acts like a dictionary.
The key is the string prefix that mcnp uses but is case insensitive.
So controlling the printing of cell.importance
data is handled by:
problem.print_in_data_block["IMP"]
.
Most of the work with this property is automated.
Syntax Objects: ParsingNode
#
This represents all low level components in MCNP syntax, such as:
Messages, titles, and Inputs.
Similar to MCNP_Object
you will need to implement format_for_mcnp_input
.
In this case though you will not have access the nice helper functions.
You will be responsible for ensuring that you do not exceed the maximum
number of column numbers allowed in a line.
How to __init__#
You need to call super().__init__(input_lines)
,
and this will provide by self.input_lines
.
Parsers: MCNP_Parser
#
This is the base class for all parsers in MontePy.
It is a wrapper for a sly.Parser
instance.
It has had to implement some janky metaclass properties in order to allow subclassing.
Warning
The new subclassing system breaks the SLY magic that allows function overloading (multiple function definitions with the same name), when subclassed. So if you define a new function with the same name as from the parent class it will hide the parent implementation, and will likely break a lot of things.
First, read the SLY Documentation.
You should also be aware of the tokens that are available.
See the tokens module: montepy.input_parser.tokens
for what tokens are available.
The tokenization process is slightly contextual.
The context is only changed by the BlockType
.
The lexers used are:
cell block:
CellLexer
.surface block:
SurfaceLexer
.data block:
DataLexer
.
Most likely you are writing a parser for parsing a complex input in the data block.
You will then be subclassing montepy.input_parser.data_parser.DataParser
.
On the use of Pointers and Generator#
First you might be saying there are no pointers in python. There are pointers you just don’t see them. If these examples aren’t clear reach out to one of the core developers.
MontePy abuses pointers a lot.
This will talk a lot like a Rust reference book about ownership and borrowing.
There aren’t true parallels in python though.
In this section ownership is considered the first instance of an object,
which should basically live for the lifetime of the problem.
For a Surface
it is owned by the Surfaces
collection owned by the MCNP_Problem
.
A cell then borrows this object by referencing it in its own Surfaces
collections.
For example:
>>> import montepy
>>> # owns
>>> x = montepy.Cell()
>>> old_id = hex(id(x))
>>> # borrows
>>> new_list = [x]
>>> old_id == hex(id(new_list[0]))
True
The general principle is that only one-directional pointers should be used, and bidirectional pointers should never be used. This is due to the maintenance overhead with mutation. For instance: a cell knows the surface objects it uses, but a surface doesn’t always know what cell object uses it. This is a one-directional pointer, if the surfaces did know, this would be bidirectional.
So how do we decide which direction to point?
In general we should default to MCNP.
So a cell borrows a surface because a cell card in MCNP
references surface numbers,
and not vice versa.
The exception to this is the case of inputs that modify another object.
For example the MT
card modifies its parent M
card.
In general the parent object should own its children modifiers.
This is an area of new development, and this may change.
So how do we get a surface to know about the cells it uses?
With generators!
First, one effectively bi-directional pointer is allowed;
inputs are allowed to point to the parent problem.
This is provided through self._problem
, and
is established by: link_to_problem()
.
With this the surface can find its cells by:
@property
def cells(self):
if self._problem:
for cell in self._problem.cells:
if self in cell.surfaces:
yield cell
So why generators and not functions? This is meant to force the data to be generated on the fly, so it is tolerant to mutation. If we were to return a list a user is much more likely to store that, and use that instead. If we make it easy to just say:
if cell in surface.cells:
pass
Users are more like to use this dynamic code. In general this philosophy is: if it’s not the source of truth, it should be a generator.