-
Notifications
You must be signed in to change notification settings - Fork 18
Implement _collection_ref to link objects to their NumberedObjectCollection parent #867
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
|
Thanks for doing this. I'll probably look at this in the next few days. |
MicahGale
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't fully evaluate how this works, but I think there are some design changes needed first.
|
// Ipynd files got updated with
|
|
Append to Collection:
Append with Renumber:
Number Changes (500 ops):
Deepcopy Problem:
Detailsimport copy
import time
import montepy
def benchmark_append_to_collection(n_objects: int) -> float:
"""Benchmark appending N objects to a fresh collection."""
collection = montepy.Cells()
cells = [montepy.Cell(f"{i+1} 0 -1") for i in range(n_objects)]
start = time.perf_counter()
for cell in cells:
collection.append(cell)
return time.perf_counter() - start
def benchmark_append_renumber(n_objects: int) -> float:
"""Benchmark appending N objects with renumbering."""
collection = montepy.Cells()
cells = [montepy.Cell(f"{i+1} 0 -1") for i in range(n_objects)]
start = time.perf_counter()
for cell in cells:
collection.append_renumber(cell)
return time.perf_counter() - start
def benchmark_number_changes(n_objects: int) -> float:
"""Benchmark changing object numbers."""
problem = montepy.read_input("tests/inputs/test.imcnp")
base_num = 10000
for i in range(n_objects):
cell = montepy.Cell(f"{base_num + i} 0 -1")
problem.cells.append(cell)
cells = list(problem.cells)
start = time.perf_counter()
for i in range(500):
cell = cells[i % len(cells)]
old_num = cell.number
cell.number = old_num + 100000
cell.number = old_num
return time.perf_counter() - start
def benchmark_deepcopy(n_objects: int) -> float:
"""Benchmark deepcopy of a problem."""
problem = montepy.read_input("tests/inputs/test.imcnp")
for i in range(n_objects):
cell = montepy.Cell(f"{1000 + i} 0 -1")
problem.cells.append(cell)
start = time.perf_counter()
_ = copy.deepcopy(problem)
return time.perf_counter() - start
def print_table(name: str, sizes: list[int], benchmark_func):
"""Run benchmark and print simple table."""
print(f"\n{name}:")
print(f"{'Objects':<10} | {'Time (s)':<12}")
print(f"{'-'*10} | {'-'*12}")
for size in sizes:
elapsed = benchmark_func(size)
print(f"{size:<10} | {elapsed:<12.4f}")
def main():
print("=" * 40)
print("MontePy _collection_ref Benchmark")
print("=" * 40)
sizes = [100, 200, 500, 1000, 4000, 8000]
small_sizes = [100, 200, 500, 1000]
print_table("Append to Collection", sizes, benchmark_append_to_collection)
print_table("Append with Renumber", sizes, benchmark_append_renumber)
print_table("Number Changes (500 ops)", small_sizes, benchmark_number_changes)
print_table("Deepcopy Problem", sizes, benchmark_deepcopy)
# Sanity check
print(f"\n{'='*40}")
print("Sanity Check:")
problem = montepy.read_input("tests/inputs/test.imcnp")
cell = list(problem.cells)[0]
if hasattr(cell, "_collection_ref") and cell._collection is not None:
print(f" ✓ _collection_ref is working")
else:
print(f" ✗ _collection_ref is NOT set")
print("=" * 40)
if __name__ == "__main__":
main() |
MicahGale
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big improvement, and much more like what I was thinking. I have some more feedback, but it is not major.
Looking at the data you collected this does look like it is indeed linear (O(N)), so that's great that it is no longer O(N^2). I have been thinking more about how to present performance data recently. I think in general, rates should generally be used (e.g., objects/second) over just raw time. My last paper on this though did not use rates, so I'm working on this as well.
doc/source/changelog.rst
Outdated
|
|
||
| **Features Added** | ||
|
|
||
| * Implement _collection_ref to link objects to their NumberedObjectCollection parent (:issue:`867`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just made a commit to your branch to re-organize the changelog. This won't be in 1.2.0, obviously because that ship has sailed. I haven't decided yet if this will be in 1.2.1 or 1.3.0.
| result._collection_ref = None | ||
| result._problem_ref = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still not the desired behavior in all cases. Generally they should be added to the same problem. I know that this can lead to a number collision in an edge case with hypothesis, that I haven't fixed yet, and I just ignore it with rm -r .hypothesis. I think that's a separate issue from this, and I need to open a bug report. Try it without these lines, and with purging the .hypothesis folder.
| collection = getattr(self._problem, collection_type.__name__.lower()) | ||
|
|
||
| # Only validate against collection if linked to a problem | ||
| if self._problem is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can skip a level of ifs and just check if self._collection is not None.
| else: | ||
| # Find collection via _problem | ||
| obj_map = montepy.MCNP_Problem._NUMBERED_OBJ_MAP | ||
| collection_type = obj_map.get(type(self)) | ||
|
|
||
| if collection_type is None: | ||
| # Finding via inheritance | ||
| for obj_class in obj_map: | ||
| if isinstance(self, obj_class): | ||
| collection_type = obj_map[obj_class] | ||
| break | ||
|
|
||
| if collection_type is not None: | ||
| collection = getattr(self._problem, collection_type.__name__.lower()) | ||
| else: | ||
| raise TypeError( | ||
| f"Could not find collection type for {type(self).__name__} in problem." | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can delete this whole branch. I don't see a case where self._problem is linked, but self._collection is not.
| if "_collection_ref" in state: | ||
| del state["_collection_ref"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good code. Could also use state.pop("_collection_ref", None)
|
|
||
| @property | ||
| def _collection(self): | ||
| """Returns the parent collection this object belongs to, if any.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to also reimplement link_to_problem here. It should at least:
- call
super().link_to_problem - grab the relevant collection from
problem. See the logic above in_validate_number. - create a
weakref.refto that collection (in_collection_ref).
This will be the secondary way to update this ref, besides appending.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though this may lead to weird situations where objects are linked to problems without being in the collections. I'm going to belay this until I read further.
| return self._problem_ref() | ||
| return None | ||
|
|
||
| def _link_to_collection(self, obj): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry this is the flip of what I meant.
I think these should be functions of NumberedMCNP_Object, and then called from _internal_append.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason being is objects should respect each other's private attributes. I am ok with calling each other's private functions as that in my mind is more marking it as internal use only.
| ) | ||
| self.__num_cache[obj.number] = obj | ||
| self._objects.append(obj) | ||
| self._link_to_collection(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be obj._link_to_collection(self)
| """ | ||
| self.__num_cache.pop(obj.number, None) | ||
| self._objects.remove(obj) | ||
| self._unlink_from_collection(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also obj._unlink_from_collection(self).
Pull Request Checklist for MontePy
Description
Added a
_collection_refas a weak_ref which points the object back to its collection.Problems
deepcopygot messed up link "number already in use" another failed test due to surface conflicts._cellpointed to wrong cell objects.Solution:
Rebuild cache after
deepcopyinstead of overwriting_collection_ref.Added
_set_cell methodtohalfspaceDetails
Fixes #849
General Checklist
blackversion 25.LLM Disclosure
Were any large language models (LLM or "AI") used in to generate any of this code?
Documentation Checklist
First-Time Contributor Checklist
pyproject.tomlif you wish to do so.Additional Notes for Reviewers
Ensure that:
📚 Documentation preview 📚: https://montepy--867.org.readthedocs.build/en/867/