Skip to content

Conversation

@digvijay-y
Copy link
Contributor

@digvijay-y digvijay-y commented Dec 17, 2025

Pull Request Checklist for MontePy

Description

Added a _collection_ref as a weak_ref which points the object back to its collection.

Problems

deepcopy got messed up link "number already in use" another failed test due to surface conflicts.
_cell pointed to wrong cell objects.

Solution:

Rebuild cache after deepcopy instead of overwriting _collection_ref.
Added _set_cell method to halfspace

    100 objects -> 0.127 seconds
    500 objects -> 0.443 seconds
   1000 objects -> 0.937 seconds
   8000 objects -> 8.385 seconds
Details
import time
import montepy


class Collection(montepy.Cells):
  """Use the real Cells collection for testing"""

  pass


def dump_stats(n):
  collection = Collection()
  start = time.time()
  for i in range(n):
      cell = montepy.Cell(f"{i+1} 0 -1")
      collection.append_renumber(cell)
  end = time.time()
  print(f"{n:>7d} objects -> {end - start:.3f} seconds")


if __name__ == "__main__":
  print("Performance test: append_renumber with _collection weakrefs")
  print("=" * 60)
  for n_cells in [100, 500, 1000, 8000]:
      dump_stats(n_cells)
  print("=" * 60)

Fixes #849


General Checklist

  • I have performed a self-review of my own code.
  • The code follows the standards outlined in the development documentation.
  • I have formatted my code with black version 25.
  • I have added tests that prove my fix is effective or that my feature works (if applicable).

LLM Disclosure

Were any large language models (LLM or "AI") used in to generate any of this code?

  • Yes
    • Model(s) used:
    • Copilot
  • No

Documentation Checklist

  • I have documented all added classes and methods.
  • For infrastructure updates, I have updated the developer's guide.
  • For significant new features, I have added a section to the getting started guide.

First-Time Contributor Checklist

  • If this is your first contribution, add yourself to pyproject.toml if you wish to do so.

Additional Notes for Reviewers

Ensure that:

  • This PR fully addresses and resolves the referenced issue(s).
  • The submitted code is consistent with the merge checklist outlined here.
  • The PR covers all relevant aspects according to the development guidelines.
  • 100% coverage of the patch is achieved, or justification for a variance is given.

📚 Documentation preview 📚: https://montepy--867.org.readthedocs.build/en/867/

@digvijay-y digvijay-y changed the title Weakref : Collection_ref Implement _collection_ref to link objects to their NumberedObjectCollection parent Dec 17, 2025
@MicahGale
Copy link
Collaborator

Thanks for doing this. I'll probably look at this in the next few days.

Copy link
Collaborator

@MicahGale MicahGale left a 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.

@digvijay-y
Copy link
Contributor Author

digvijay-y commented Jan 5, 2026

// Ipynd files got updated with black

  1. Moved _collection_ref to Numbered_MCNP_Object
  2. Removed _set_cell methods
  3. Fixed _number_validator to raise error
  4. Created _link_to_collection method
  5. Removed cache rebuild
  6. Fixed try block error message

@digvijay-y
Copy link
Contributor Author

Append to Collection:

Objects Time (s)
100 0.0003
200 0.0006
500 0.0014
1000 0.0030
4000 0.0121
8000 0.0448

Append with Renumber:

Objects Time (s)
100 0.0005
200 0.0010
500 0.0027
1000 0.0055
4000 0.0224
8000 0.0434

Number Changes (500 ops):

Objects Time (s)
100 0.0048
200 0.0041
500 0.0043
1000 0.0043

Deepcopy Problem:

Objects Time (s)
100 0.3575
200 0.4597
500 1.4791
1000 2.8051
4000 11.0377
8000 21.7734
Details
import 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()

@digvijay-y digvijay-y requested a review from MicahGale January 5, 2026 13:59
Copy link
Collaborator

@MicahGale MicahGale left a 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.


**Features Added**

* Implement _collection_ref to link objects to their NumberedObjectCollection parent (:issue:`867`).
Copy link
Collaborator

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.

Comment on lines +932 to +933
result._collection_ref = None
result._problem_ref = None
Copy link
Collaborator

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:
Copy link
Collaborator

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.

Comment on lines +23 to +41
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."
)

Copy link
Collaborator

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.

Comment on lines +148 to +149
if "_collection_ref" in state:
del state["_collection_ref"]
Copy link
Collaborator

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."""
Copy link
Collaborator

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:

  1. call super().link_to_problem
  2. grab the relevant collection from problem. See the logic above in _validate_number.
  3. create a weakref.ref to that collection (in _collection_ref).

This will be the secondary way to update this ref, besides appending.

Copy link
Collaborator

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):
Copy link
Collaborator

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.

Copy link
Collaborator

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)
Copy link
Collaborator

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)
Copy link
Collaborator

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Link objects to their NumberedObjectCollection Parent

2 participants