diff --git a/demo/0_IntroToMontePy.ipynb b/demo/0_IntroToMontePy.ipynb index 56312f06..c267ce89 100644 --- a/demo/0_IntroToMontePy.ipynb +++ b/demo/0_IntroToMontePy.ipynb @@ -228,8 +228,9 @@ }, "outputs": [], "source": [ - "#note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", + "# note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", @@ -428,7 +429,7 @@ }, "outputs": [], "source": [ - "#note this %pip is only needed for running in jupyterlite online\n", + "# note this %pip is only needed for running in jupyterlite online\n", "%pip install ipython\n", "from _config import IFrame\n", "\n", diff --git a/demo/1_PinCellCorrection_inter.ipynb b/demo/1_PinCellCorrection_inter.ipynb index 934ff656..7577050c 100644 --- a/demo/1_PinCellCorrection_inter.ipynb +++ b/demo/1_PinCellCorrection_inter.ipynb @@ -121,8 +121,9 @@ }, "outputs": [], "source": [ - "#note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", + "# note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "# actually needed\n", @@ -234,7 +235,9 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.MCNP_Problem.html#montepy.MCNP_Problem\")" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.MCNP_Problem.html#montepy.MCNP_Problem\"\n", + ")" ] }, { @@ -304,7 +307,9 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.Surfaces.html#montepy.Surfaces\")" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.Surfaces.html#montepy.Surfaces\"\n", + ")" ] }, { @@ -374,7 +379,9 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.Surface.html#montepy.Surface\")" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.Surface.html#montepy.Surface\"\n", + ")" ] }, { diff --git a/demo/2_BuildAssembly_inter.ipynb b/demo/2_BuildAssembly_inter.ipynb index 89db4008..766331bf 100644 --- a/demo/2_BuildAssembly_inter.ipynb +++ b/demo/2_BuildAssembly_inter.ipynb @@ -65,6 +65,7 @@ "outputs": [], "source": [ "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", diff --git a/demo/3_AddingGuideTubes_inter.ipynb b/demo/3_AddingGuideTubes_inter.ipynb index 9be2c4be..dc6af1cb 100644 --- a/demo/3_AddingGuideTubes_inter.ipynb +++ b/demo/3_AddingGuideTubes_inter.ipynb @@ -105,6 +105,7 @@ "outputs": [], "source": [ "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", diff --git a/demo/4_AxialDiscretize_inter.ipynb b/demo/4_AxialDiscretize_inter.ipynb index c1cffafa..a0241fb9 100644 --- a/demo/4_AxialDiscretize_inter.ipynb +++ b/demo/4_AxialDiscretize_inter.ipynb @@ -71,6 +71,7 @@ "outputs": [], "source": [ "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", diff --git a/demo/answers/1_PinCellCorrection.ipynb b/demo/answers/1_PinCellCorrection.ipynb index edc645f7..6d6a6274 100644 --- a/demo/answers/1_PinCellCorrection.ipynb +++ b/demo/answers/1_PinCellCorrection.ipynb @@ -120,8 +120,9 @@ }, "outputs": [], "source": [ - "#note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", + "# note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "# actually needed\n", @@ -233,7 +234,11 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.MCNP_Problem.html#montepy.MCNP_Problem\", 800, 600)" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.MCNP_Problem.html#montepy.MCNP_Problem\",\n", + " 800,\n", + " 600,\n", + ")" ] }, { @@ -317,7 +322,9 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.Surfaces.html#montepy.Surfaces\")" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.Surfaces.html#montepy.Surfaces\"\n", + ")" ] }, { @@ -402,7 +409,9 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.Surface.html#montepy.Surface\")" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.Surface.html#montepy.Surface\"\n", + ")" ] }, { @@ -1561,7 +1570,9 @@ }, "outputs": [], "source": [ - "IFrame(\"https://www.montepy.org/en/stable/api/generated/montepy.MCNP_Problem.html#montepy.MCNP_Problem\")" + "IFrame(\n", + " \"https://www.montepy.org/en/stable/api/generated/montepy.MCNP_Problem.html#montepy.MCNP_Problem\"\n", + ")" ] }, { diff --git a/demo/answers/2_BuildAssembly.ipynb b/demo/answers/2_BuildAssembly.ipynb index b651fa4f..ddb3e536 100644 --- a/demo/answers/2_BuildAssembly.ipynb +++ b/demo/answers/2_BuildAssembly.ipynb @@ -64,8 +64,9 @@ }, "outputs": [], "source": [ - "#note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", + "# note this install_montepy is only necessary for jupyterlite; You don't need to use it locally\n", "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", diff --git a/demo/answers/3_AddingGuideTubes.ipynb b/demo/answers/3_AddingGuideTubes.ipynb index f21e9bfa..bcb5c3ab 100644 --- a/demo/answers/3_AddingGuideTubes.ipynb +++ b/demo/answers/3_AddingGuideTubes.ipynb @@ -104,6 +104,7 @@ "outputs": [], "source": [ "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", diff --git a/demo/answers/4_AxialDiscretize.ipynb b/demo/answers/4_AxialDiscretize.ipynb index a70f87f7..8ffe339d 100644 --- a/demo/answers/4_AxialDiscretize.ipynb +++ b/demo/answers/4_AxialDiscretize.ipynb @@ -71,6 +71,7 @@ "outputs": [], "source": [ "from _config import install_montepy\n", + "\n", "install_montepy()\n", "\n", "import montepy\n", diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 3b805cbd..a5f3ab2c 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -5,6 +5,13 @@ MontePy Changelog 1.2 releases ============ +#Next Version# +-------------- + +**Performance Improvement** + +* Implement _collection_ref to link objects to their NumberedObjectCollection parent (:issue:`867`). + 1.2.0 -------------- diff --git a/montepy/cell.py b/montepy/cell.py index ce23a5eb..39095400 100644 --- a/montepy/cell.py +++ b/montepy/cell.py @@ -927,6 +927,10 @@ def num(obj): for key in keys: attr = getattr(self, key) setattr(result, key, copy.deepcopy(attr, memo)) + # Clear weakrefs so the cloned cell isn't linked to original collection/problem + # This prevents number conflict checks against the original collection + result._collection_ref = None + result._problem_ref = None # copy geometry for special in special_keys: new_objs = [] diff --git a/montepy/numbered_mcnp_object.py b/montepy/numbered_mcnp_object.py index 6137b1ce..c4c6759f 100644 --- a/montepy/numbered_mcnp_object.py +++ b/montepy/numbered_mcnp_object.py @@ -5,6 +5,7 @@ import itertools from typing import Union from numbers import Integral +import weakref from montepy.mcnp_object import MCNP_Object, InitInput import montepy @@ -14,20 +15,10 @@ def _number_validator(self, number): if number < 0: raise ValueError("number must be >= 0") - if self._problem: - obj_map = montepy.MCNP_Problem._NUMBERED_OBJ_MAP - try: - collection_type = obj_map[type(self)] - except KeyError as e: - found = False - for obj_class in obj_map: - if isinstance(self, obj_class): - collection_type = obj_map[obj_class] - found = True - break - if not found: - raise e - collection = getattr(self._problem, collection_type.__name__.lower()) + + # Only validate against collection if linked to a collection + if self._collection is not None: + collection = self._collection collection.check_number(number) collection._update_number(self.number, number, self) @@ -58,6 +49,7 @@ def __init__( if not input: self._number = self._generate_default_node(int, -1) super().__init__(input, parser) + self._collection_ref = None self._load_init_num(number) def _load_init_num(self, number): @@ -123,6 +115,37 @@ def _add_children_objs(self, problem): except (TypeError, AssertionError): prob_collect.append(child_collect) + @property + def _collection(self): + """Returns the parent collection this object belongs to, if any.""" + if self._collection_ref is not None: + return self._collection_ref() + return None + + def _link_to_collection(self, collection): + """Links this object to the given collection via a weakref. + + Parameters + ---------- + collection : NumberedObjectCollection + The collection to link this object to. + """ + self._collection_ref = weakref.ref(collection) + + def _unlink_from_collection(self): + """Unlinks this object from its collection.""" + self._collection_ref = None + + def __getstate__(self): + state = super().__getstate__() + # Remove _collection_ref weakref as it can't be pickled + state.pop("_collection_ref", None) + return state + + def __setstate__(self, crunchy_data): + crunchy_data["_collection_ref"] = None + super().__setstate__(crunchy_data) + def clone(self, starting_number=None, step=None): """Create a new independent instance of this object with a new number. diff --git a/montepy/numbered_object_collection.py b/montepy/numbered_object_collection.py index 52ceb32f..24036c8a 100644 --- a/montepy/numbered_object_collection.py +++ b/montepy/numbered_object_collection.py @@ -178,6 +178,7 @@ def __init__( ) ) self.__num_cache[obj.number] = obj + self._link_to_collection(obj) self._objects = objects def link_to_problem(self, problem): @@ -198,6 +199,11 @@ def link_to_problem(self, problem): self._problem_ref = weakref.ref(problem) for obj in self: obj.link_to_problem(problem) + # the _collection_ref that points to the main cells collection. + if problem is not None: + existing_coll = obj._collection + if existing_coll is None or existing_coll._problem is not problem: + self._link_to_collection(obj) @property def _problem(self): @@ -205,6 +211,26 @@ def _problem(self): return self._problem_ref() return None + def _link_to_collection(self, obj): + """Links the given object to this collection via a weakref. + + Parameters + ---------- + obj : Numbered_MCNP_Object + The object to link to this collection. + """ + obj._link_to_collection(self) + + def _unlink_from_collection(self, obj): + """Unlinks the given object from this collection. + + Parameters + ---------- + obj : Numbered_MCNP_Object + The object to unlink from this collection. + """ + obj._unlink_from_collection() + def __getstate__(self): state = self.__dict__.copy() weakref_key = "_problem_ref" @@ -341,10 +367,8 @@ def extend(self, other_list): ) if obj.number in nums: raise NumberConflictError( - ( - f"When adding to {type(self).__name__} there was a number collision due to " - f"adding {obj} which conflicts with {self[obj.number]}" - ) + f"When adding to {type(self).__name__} there was a number collision due to " + f"adding {obj} which conflicts with existing object number {obj.number}" ) nums.add(obj.number) for obj in other_list: @@ -496,6 +520,7 @@ def __internal_append(self, obj, **kwargs): ) self.__num_cache[obj.number] = obj self._objects.append(obj) + self._link_to_collection(obj) self._append_hook(obj, **kwargs) if self._problem: obj.link_to_problem(self._problem) @@ -507,6 +532,7 @@ def __internal_delete(self, obj, **kwargs): """ self.__num_cache.pop(obj.number, None) self._objects.remove(obj) + self._unlink_from_collection(obj) self._delete_hook(obj, **kwargs) def add(self, obj: Numbered_MCNP_Object): @@ -613,6 +639,7 @@ def append_renumber(self, obj, step=1): number = obj.number if obj.number > 0 else 1 if self._problem: obj.link_to_problem(self._problem) + self._unlink_from_collection(obj) try: self.append(obj) except (NumberConflictError, ValueError) as e: diff --git a/tests/test_numbered_collection.py b/tests/test_numbered_collection.py index e5335cd4..1eb7fcde 100644 --- a/tests/test_numbered_collection.py +++ b/tests/test_numbered_collection.py @@ -86,6 +86,7 @@ def test_extend(self, cp_simple_problem): extender = copy.deepcopy(extender) for surf in extender: surf._problem = None + surf._collection_ref = None surfaces[1000].number = 1 extender[0].number = 1000 extender[1].number = 70 @@ -161,6 +162,7 @@ def test_append_renumber(self, cp_simple_problem): cells.append_renumber(cell, "hi") cell = copy.deepcopy(cell) cell._problem = None + cell._collection_ref = None cell.number = 1 cells.append_renumber(cell) assert cell.number == 4