Repository: BYUCamachoLab/simphony

Issue:
Model objects should take a name option in the constructor
Model objects have a setter to set their name here, and a property for it here

https://github.com/BYUCamachoLab/simphony/blob/683843ccec7d59f39d6e4724f1e65def362349ac/simphony/models.py#L363C9-L363C9

but they don't take an optional name argument in their constructor, so it requires 2 calls to set the name of a model object at the moment. Should add the optional `name` kwarg to the constructor.

Discussion:

Patch:
diff --git a/docs/_config.yml b/docs/_config.yml
index 676172a..af539f1 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -70,7 +70,7 @@ sphinx:
   config:
     autosummary_generate: true
     autodoc_typehints: 'signature'
-    autodoc_mock_imports: []
+    autodoc_mock_imports: ["SiPANN", "parsimonious", "jax"]
     autodoc_default_options: {"show-inheritance": True}
     # autoclass_content: "class
     autodoc_inherit_docstrings: true
diff --git a/docs/tutorials/intro.ipynb b/docs/tutorials/intro.ipynb
index 60abde6..6d1e2c3 100644
--- a/docs/tutorials/intro.ipynb
+++ b/docs/tutorials/intro.ipynb
@@ -77,7 +77,10 @@
     "class CustomModel(Model):\n",
     "    onames = [\"o0\", \"o1\"] # indicates the names of the optical ports of the model\n",
     "\n",
-    "    def __init__(self, param: float = 0.5): # this model will have one parameter\n",
+    "    # This model will have one parameter, param, which defaults to 0.5\n",
+    "    # It can also always take an optional name, for easier identification\n",
+    "    def __init__(self, param: float = 0.5, name: str = None):\n",
+    "        super().__init__(name=name) # super().__init__() is required\n",
     "        self.param = param\n",
     "\n",
     "    def s_params(self, wl):\n",
diff --git a/pyproject.toml b/pyproject.toml
index 8477ee2..d8e5f21 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -73,7 +73,6 @@ doc = [
   "jupyter-book ~= 0.13.2",
   "sphinx-design ~= 0.1.0",
   "matplotlib",
-  "numpy",
   "numpydoc",
   # sphinx-copybutton==0.5.1
   # sphinx-togglebutton==0.3.2
diff --git a/simphony/circuit.py b/simphony/circuit.py
index e3e409b..6adc900 100644
--- a/simphony/circuit.py
+++ b/simphony/circuit.py
@@ -14,7 +14,7 @@ import numpy as np
 
 from simphony.connect import connect_s, vector_innerconnect_s
 from simphony.exceptions import ModelValidationError, SeparatedCircuitError
-from simphony.models import EPort, Model, OPort, Port
+from simphony.models import _NAME_REGISTER, EPort, Model, OPort, Port
 
 log = logging.getLogger(__name__)
 
@@ -79,10 +79,23 @@ class Circuit(Model):
     """
 
     _exempt = True
-    _next_idx = count()
 
     def __init__(self, name: str = None) -> None:
-        self.name = name or f"circuit{next(self._next_idx)}"
+        if name:
+            if name in _NAME_REGISTER:
+                raise ValueError(
+                    f"Name '{name}' is already in use. Please choose a different name."
+                )
+            else:
+                _NAME_REGISTER.add(name)
+                self._name = name
+        else:
+            name = self.__class__.__name__ + str(next(self.counter))
+            while name in _NAME_REGISTER:
+                name = self.__class__.__name__ + str(next(self.counter))
+            else:
+                _NAME_REGISTER.add(name)
+                self._name = name
 
         self._components = []  # list of model instances in the circuit
         self._onodes: list[tuple[OPort, OPort]] = []  # optical netlist
diff --git a/simphony/libraries/ideal.py b/simphony/libraries/ideal.py
index a7f1dff..98851d7 100644
--- a/simphony/libraries/ideal.py
+++ b/simphony/libraries/ideal.py
@@ -65,7 +65,9 @@ class Coupler(Model):
         coupling: float = 0.5,
         phi: float = jnp.pi / 2,
         loss: Union[float, Tuple[float]] = 0.0,
+        name: str = None,
     ):
+        super().__init__(name=name)
         if not 0 <= coupling <= 1:
             raise ValueError("coupling must be between 0 and 1")
         self.coupling = coupling
@@ -183,7 +185,9 @@ class Waveguide(Model):
         neff: float = None,
         ng: float = None,
         loss: float = None,
+        name: str = None,
     ):
+        super().__init__(name=name)
         self.length = length
         self.wl0 = wl0
         self.neff = neff
@@ -222,8 +226,10 @@ class PhaseShifter(Model):
         Loss of the phase shifter in dB. Defaults to 0.
     """
 
-    def __init__(self, onames=["o0", "o1"], phase=0, loss=0):
-        self.onames = onames
+    onames = ["o0", "o1"]
+
+    def __init__(self, phase: float = 0, loss: float = 0, name: str = None):
+        super().__init__(name=name)
         self.phase = phase
         self.loss = loss
 
diff --git a/simphony/libraries/siepic/__init__.py b/simphony/libraries/siepic/__init__.py
index 4273cfb..0562f40 100644
--- a/simphony/libraries/siepic/__init__.py
+++ b/simphony/libraries/siepic/__init__.py
@@ -83,7 +83,8 @@ class BidirectionalCouplerTE(Model):
 
     ocount = 4
 
-    def __init__(self, pol="te", thickness=220.0, width=500):
+    def __init__(self, pol="te", thickness=220.0, width=500, name: str = None):
+        super().__init__(name=name)
         if pol not in ["te", "tm"]:
             raise ValueError("'pol' must be one of 'te' or 'tm'")
         self.pol = pol
@@ -169,7 +170,8 @@ class DirectionalCoupler(Model):
 
     ocount = 4
 
-    def __init__(self, gap=200, coupling_length=0):
+    def __init__(self, gap=200, coupling_length=0, name: str = None):
+        super().__init__(name=name)
         df = self._generate_parameter_sets()
         if not ((df["gap"] == gap) & (df["coupling_length"] == coupling_length)).any():
             raise ValueError(
@@ -283,7 +285,8 @@ class GratingCoupler(Model):
 
     ocount = 2
 
-    def __init__(self, pol="te", thickness=220.0, dwidth=0):
+    def __init__(self, pol="te", thickness=220.0, dwidth=0, name: str = None):
+        super().__init__(name=name)
         if pol not in ["te", "tm"]:
             raise ValueError("'pol' must be either 'te' or 'tm'")
         self.pol = pol.upper()
@@ -407,8 +410,16 @@ class HalfRing(Model):
     ocount = 4
 
     def __init__(
-        self, pol="te", gap=50, radius=5, width=500, thickness=220, coupling_length=0
+        self,
+        pol="te",
+        gap=50,
+        radius=5,
+        width=500,
+        thickness=220,
+        coupling_length=0,
+        name: str = None,
     ):
+        super().__init__(name=name)
         if pol not in ["te", "tm"]:
             raise ValueError("'pol' must be one of 'te' or 'tm'")
 
@@ -711,7 +722,10 @@ class Taper(Model):
 
     ocount = 2
 
-    def __init__(self, w1: float = 0.5, w2: float = 1.0, length: float = 10.0):
+    def __init__(
+        self, w1: float = 0.5, w2: float = 1.0, length: float = 10.0, name: str = None
+    ):
+        super().__init__(name=name)
         df = self._generate_parameter_sets()
         if not ((df["w1"] == w1) & (df["w2"] == w2) & (df["length"] == length)).any():
             raise ValueError(
@@ -798,7 +812,8 @@ class Terminator(Model):
 
     ocount = 1
 
-    def __init__(self, pol="te"):
+    def __init__(self, pol="te", name: str = None):
+        super().__init__(name=name)
         if pol not in ["te", "tm"]:
             raise ValueError("'pol' must be one of 'te' or 'tm'")
         self.pol = pol
@@ -1052,7 +1067,9 @@ class Waveguide(Model):
         sigma_ne=0.05,
         sigma_ng=0.05,
         sigma_nd=0.0001,
+        name: str = None,
     ):
+        super().__init__(name=name)
         if pol not in ["te", "tm"]:
             raise ValueError("Invalid polarization, must be either 'te' or 'tm'")
 
@@ -1176,7 +1193,8 @@ class YBranch(Model):
     ocount = 3
     _POL_MAPPING = {"te": 1, "tm": 2}
 
-    def __init__(self, pol="te", thickness=220.0, width=500):
+    def __init__(self, pol="te", thickness=220.0, width=500, name: str = None):
+        super().__init__(name=name)
         if pol not in ["te", "tm"]:
             raise ValueError("'pol' must be one of 'te' or 'tm'")
         self.pol = pol
diff --git a/simphony/libraries/sipann.py b/simphony/libraries/sipann.py
index b11ddd4..48b82b3 100644
--- a/simphony/libraries/sipann.py
+++ b/simphony/libraries/sipann.py
@@ -14,8 +14,15 @@ from pathlib import Path
 from typing import Callable, Dict, TypeVar, Union
 
 import numpy as np
-from SiPANN import comp, scee
-from SiPANN.scee_opt import premade_coupler
+
+try:
+    from SiPANN import comp, scee
+    from SiPANN.scee_opt import premade_coupler
+except ImportError:
+    raise ImportError(
+        "SiPANN must be installed to use the SiPANN wrappers. "
+        "To install SiPANN, run `pip install SiPANN`."
+    )
 
 from simphony.models import Model
 
@@ -43,7 +50,13 @@ class SipannWrapper(Model):
         needed, pass in an empty dictionary.
     """
 
-    def __init__(self, model: TypeVar("M"), sigmas: Dict[str, float]) -> None:
+    def __init__(
+        self,
+        model: TypeVar("M"),
+        sigmas: Dict[str, float],
+        name: str = None,
+    ) -> None:
+        super().__init__(name=name)
         self.model = model
         self.sigmas = sigmas
 
@@ -165,7 +178,14 @@ class GapFuncSymmetric(SipannWrapper):
         zmax: float,
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.GapFuncSymmetric(
                 width,
@@ -177,6 +197,7 @@ class GapFuncSymmetric(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -215,7 +236,7 @@ class GapFuncAntiSymmetric(SipannWrapper):
     width : float
         Width of waveguides in nanometers (valid from 400 to 600).
     thickness : float
-        Thickness of waveguides in meters (Valid from 180e-9 to 240e-9).
+        Thickness of waveguides in nanometers (valid from 180 to 240).
     gap : callable
         Gap function along the waveguide, values it returns must be in
         nanometers (and must always be greater than 100).
@@ -254,7 +275,16 @@ class GapFuncAntiSymmetric(SipannWrapper):
         arc4: float,
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: dict = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.GapFuncAntiSymmetric(
                 width,
@@ -269,6 +299,7 @@ class GapFuncAntiSymmetric(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -302,14 +333,14 @@ class HalfRing(SipannWrapper):
     width : float
         Width of waveguides in nanometers (valid from 400 to 600).
     thickness : float
-        Thickness of waveguides in meters (Valid from 180e-9 to 240e-9).
+        Thickness of waveguides in nanometers (valid from 180 to 240).
     radius : float
         Distance from center of ring to middle of waveguide, in nanometers.
     gap : float
         Minimum distance from ring waveguide edge to straight waveguide edge,
         in nanometers (must be greater than 100).
     sw_angle : float, optional
-        Sidewall angle of waveguide from horizontal in degrees (Valid from 80
+        Sidewall angle of waveguide from horizontal in degrees (valid from 80
         to 90, defaults to 90).
     sigmas : dict, optional
         Dictionary mapping parameters to sigma values for Monte-Carlo
@@ -330,7 +361,16 @@ class HalfRing(SipannWrapper):
         gap: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.HalfRing(
                 width,
@@ -340,6 +380,7 @@ class HalfRing(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     def s_params(self, wl):
@@ -419,7 +460,16 @@ class HalfRacetrack(SipannWrapper):
         length: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.HalfRacetrack(
                 width,
@@ -430,6 +480,7 @@ class HalfRacetrack(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -463,7 +514,7 @@ class StraightCoupler(SipannWrapper):
     width : float
         Width of waveguides in nanometers (valid from 400 to 600).
     thickness : float
-        Thickness of waveguides in nanometers (balid from 180 to 240).
+        Thickness of waveguides in nanometers (valid from 180 to 240).
     gap : float
         Distance between the two waveguide edges, in nanometers (must be greater
         than 100).
@@ -491,7 +542,16 @@ class StraightCoupler(SipannWrapper):
         length: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.StraightCoupler(
                 width,
@@ -501,6 +561,7 @@ class StraightCoupler(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -571,7 +632,16 @@ class StandardCoupler(SipannWrapper):
         vertical: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.Standard(
                 width,
@@ -583,6 +653,7 @@ class StandardCoupler(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -648,7 +719,16 @@ class DoubleHalfRing(SipannWrapper):
         gap: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.DoubleHalfRing(
                 width,
@@ -658,6 +738,7 @@ class DoubleHalfRing(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -728,7 +809,16 @@ class AngledHalfRing(SipannWrapper):
         theta: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.AngledHalfRing(
                 width,
@@ -739,6 +829,7 @@ class AngledHalfRing(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -791,10 +882,18 @@ class Waveguide(SipannWrapper):
         length: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             scee.Waveguide(width, thickness, length, sw_angle),
             sigmas,
+            name=name,
         )
 
     # def update_variations(self, **kwargs):
@@ -858,7 +957,16 @@ class Racetrack(SipannWrapper):
         length: Union[float, np.ndarray],
         sw_angle: Union[float, np.ndarray] = 90,
         sigmas: Dict[str, float] = dict(),
+        name: str = None,
     ) -> None:
+        if width < 400 or width > 600:
+            raise ValueError("Width must be between 400 and 600 nm")
+        if thickness < 180 or thickness > 240:
+            raise ValueError("Thickness must be between 180 and 240 nm")
+        if gap < 100:
+            raise ValueError("Gap must be greater than 100 nm")
+        if sw_angle < 80 or sw_angle > 90:
+            raise ValueError("Sidewall angle must be between 80 and 90 degrees")
         super().__init__(
             comp.racetrack_sb_rr(
                 width,
@@ -869,6 +977,7 @@ class Racetrack(SipannWrapper):
                 sw_angle,
             ),
             sigmas,
+            name=name,
         )
 
     def write_gds(self, filename: Union[Path, str]) -> None:
@@ -924,5 +1033,7 @@ class PremadeCoupler(SipannWrapper):
 
     ocount = 4
 
-    def __init__(self, split: int, sigmas: Dict[str, float] = dict(), **kwargs) -> None:
-        super().__init__(premade_coupler(split)[0], sigmas, **kwargs)
+    def __init__(
+        self, split: int, sigmas: Dict[str, float] = dict(), name: str = None, **kwargs
+    ) -> None:
+        super().__init__(premade_coupler(split)[0], sigmas, name=name, **kwargs)
diff --git a/simphony/models.py b/simphony/models.py
index 8dca3d6..92830a8 100644
--- a/simphony/models.py
+++ b/simphony/models.py
@@ -32,6 +32,9 @@ if TYPE_CHECKING:
 log = logging.getLogger(__name__)
 
 
+_NAME_REGISTER = set()
+
+
 class Port:
     """Port abstract base class containing name and reference to Model
     instance."""
@@ -218,7 +221,7 @@ class Model:
 
     # Default keys to ignore when checking for equality or hashing. Private
     # attributes are always ignored when checking for equality.
-    _ignore_keys = ["onames", "ocount", "enames", "ecount", "icount", "counter"]
+    _ignore_keys = ["onames", "ocount", "enames", "ecount", "counter"]
     counter = count()
 
     # These should always be instance attributes, not treated as class
@@ -227,7 +230,7 @@ class Model:
     _oports: list[OPort] = []  # should only be manipulated by rename_oports()
     _eports: list[EPort] = []  # should only be manipulated by rename_eports()
 
-    def _validate(self) -> None:
+    def __init__(self, name: str = None) -> None:
         if hasattr(self, "_exempt"):
             return
 
@@ -249,25 +252,30 @@ class Model:
         elif hasattr(self, "ecount"):
             self.rename_eports([f"o{i}" for i in range(getattr(self, "ecount"))])
 
-        self.icount = next(self.counter)
-        self._name = None
+        if name:
+            if name in _NAME_REGISTER:
+                raise ValueError(
+                    f"Name '{name}' is already in use. Please choose a different name."
+                )
+            else:
+                _NAME_REGISTER.add(name)
+                self._name = name
+        else:
+            name = self.__class__.__name__ + str(next(self.counter))
+            while name in _NAME_REGISTER:
+                name = self.__class__.__name__ + str(next(self.counter))
+            else:
+                _NAME_REGISTER.add(name)
+                self._name = name
 
-    def __init_subclass__(cls) -> None:
+    def __init_subclass__(cls, **kwargs):
         """Ensures subclasses define required functions and automatically calls
         the super().__init__ function."""
-        if not hasattr(cls, "s_params"):
+        if cls.s_params == Model.s_params:
             raise ModelValidationError(
                 f"Model '{cls.__name__}' does not define the required method 's_params(self, wl).'"
             )
-
-        orig_init = cls.__init__
-
-        @wraps(orig_init)
-        def __init__(self, *args, **kwargs):
-            orig_init(self, *args, **kwargs)
-            super(self.__class__, self)._validate()
-
-        cls.__init__ = __init__
+        super().__init_subclass__(**kwargs)
 
     def __eq__(self, other: Model):
         """Compares instance dictionaries to determine equality."""
@@ -332,7 +340,7 @@ class Model:
 
     def __repr__(self) -> str:
         """Code representation of the model."""
-        return f'<{self.__class__.__name__} at {hex(id(self))} (o: [{", ".join(["+"+o.name if o.connected else o.name for o in self._oports])}], e: [{", ".join(["+"+e.name if e.connected else e.name for e in self._eports]) or None}])>'
+        return f'<{self.__class__.__name__} "{self.name}" (o: [{", ".join(["+"+o.name if o.connected else o.name for o in self._oports])}], e: [{", ".join(["+"+e.name if e.connected else e.name for e in self._eports]) or None}])>'
 
     def __iter__(self):
         """Iterate over unconnected ports."""
@@ -364,7 +372,7 @@ class Model:
     @property
     def name(self) -> str:
         """Returns the name of the model."""
-        return self._name or self.__class__.__name__ + str(self.icount)
+        return self._name
 
     @name.setter
     def name(self, value: str) -> None:
@@ -535,3 +543,9 @@ class Model:
         s = self._s(tuple(wl.tolist()))
         f = wl2freq(wl * 1e-6)
         return skrf.Network(f=f[::-1], s=s[::-1], f_unit="Hz")
+
+
+def clear_name_register():
+    """Clears the name register."""
+    _NAME_REGISTER.clear()
+    Model.counter = count()


Test patch:
diff --git a/tests/libraries/sipann/test_sipann_models.py b/tests/libraries/sipann/test_sipann_models.py
new file mode 100644
index 0000000..9b527aa
--- /dev/null
+++ b/tests/libraries/sipann/test_sipann_models.py
@@ -0,0 +1,208 @@
+import pytest
+import numpy as np
+
+try:
+    from simphony.libraries import sipann
+except ImportError:
+    SIPANN_AVAILABLE = False
+
+
+class TestGapFuncSymmetric:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.GapFuncSymmetric(
+                width=350,
+                thickness=160,
+                gap=(lambda x: x * 3),
+                dgap=(lambda x: 3),
+                zmin=0.0,
+                zmax=1.0,
+            )
+
+    def test_instantiable(self):
+        dev = sipann.GapFuncSymmetric(
+            width=500,
+            thickness=220,
+            gap=(lambda x: x * 3),
+            dgap=(lambda x: 3),
+            zmin=0.0,
+            zmax=1.0,
+        )
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.GapFuncSymmetric(
+            width=500,
+            thickness=220,
+            gap=(lambda x: x * 3),
+            dgap=(lambda x: 3),
+            zmin=0.0,
+            zmax=1.0,
+        )
+        s = dev.s_params(std_wl_um)
+
+
+# class TestGapFuncAntiSymmetric:
+#     def test_invalid_parameters(self):
+#         with pytest.raises(ValueError):
+#             sipann.GapFuncAntiSymmetric(gap=300)
+
+#     def test_instantiable(self):
+#         siepic.DirectionalCoupler(gap=200, coupling_length=45)
+
+#     def test_s_params(self, std_wl_um):
+#         dc = siepic.DirectionalCoupler(gap=200, coupling_length=45)
+#         s = dc.s_params(std_wl_um)
+
+
+class TestHalfRing:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.HalfRing(width=350, thickness=160, radius=5000, gap=50)
+
+    def test_instantiable(self):
+        dev = sipann.HalfRing(width=500, thickness=220, radius=5000, gap=100)
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.HalfRing(width=500, thickness=220, radius=5000, gap=100)
+        s = dev.s_params(std_wl_um)
+
+
+class TestHalfRacetrack:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.HalfRacetrack(
+                width=625, thickness=245, radius=5000, gap=100, length=1000
+            )
+
+    def test_instantiable(self):
+        dev = sipann.HalfRacetrack(
+            width=500, thickness=220, radius=5000, gap=100, length=1000
+        )
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.HalfRacetrack(
+            width=500, thickness=220, radius=5000, gap=100, length=1000
+        )
+        s = dev.s_params(std_wl_um)
+
+
+class TestStraightCoupler:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.StraightCoupler(width=400, thickness=160, gap=50, length=1000)
+
+    def test_instantiable(self):
+        dev = sipann.StraightCoupler(width=500, thickness=220, gap=150, length=1000)
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.StraightCoupler(width=500, thickness=220, gap=180, length=1000)
+        s = dev.s_params(std_wl_um)
+
+
+class TestStandardCoupler:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.StandardCoupler(
+                width=399,
+                thickness=240.1,
+                gap=180,
+                length=2000,
+                horizontal=2000,
+                vertical=2000,
+            )
+
+    def test_instantiable(self):
+        dev = sipann.StandardCoupler(
+            width=500,
+            thickness=220,
+            gap=180,
+            length=2000,
+            horizontal=2000,
+            vertical=2000,
+        )
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.StandardCoupler(
+            width=500,
+            thickness=220,
+            gap=180,
+            length=2000,
+            horizontal=2000,
+            vertical=2000,
+        )
+        s = dev.s_params(std_wl_um)
+
+
+class TestDoubleHalfRing:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.DoubleHalfRing(width=380, thickness=250, radius=5000, gap=100)
+
+    def test_instantiable(self):
+        dev = sipann.DoubleHalfRing(width=500, thickness=220, radius=5000, gap=100)
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.DoubleHalfRing(width=500, thickness=220, radius=5000, gap=100)
+        s = dev.s_params(std_wl_um)
+
+
+class TestAngledHalfRing:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.AngledHalfRing(
+                width=375, thickness=175, radius=5000, gap=150, theta=0.5
+            )
+
+    def test_instantiable(self):
+        dev = sipann.AngledHalfRing(
+            width=500, thickness=220, radius=5000, gap=150, theta=0.5
+        )
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.AngledHalfRing(
+            width=500, thickness=220, radius=5000, gap=150, theta=0.5
+        )
+        s = dev.s_params(std_wl_um)
+
+
+class TestWaveguide:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.Waveguide(width=350, thickness=250, length=10000)
+
+    def test_instantiable(self):
+        dev = sipann.Waveguide(width=500, thickness=220, length=10000)
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.Waveguide(width=500, thickness=220, length=10000)
+        s = dev.s_params(std_wl_um)
+
+
+class TestRacetrack:
+    def test_invalid_parameters(self):
+        with pytest.raises(ValueError):
+            sipann.Racetrack(width=625, thickness=175, radius=5000, gap=80, length=5000)
+
+    def test_instantiable(self):
+        dev = sipann.Racetrack(
+            width=500, thickness=220, radius=5000, gap=150, length=2000
+        )
+
+    def test_s_params(self, std_wl_um):
+        dev = sipann.Racetrack(
+            width=500, thickness=220, radius=5000, gap=150, length=2000
+        )
+        s = dev.s_params(std_wl_um)
+
+
+# class TestPremadeCoupler:
+#     def test_invalid_parameters(self):
+#         with pytest.raises(ValueError):
+#             sipann.PremadeCoupler(pol="tem")
+
+#     def test_instantiable(self):
+#         yb = siepic.YBranch(pol="te", thickness=220, width=500)
+
+#     def test_s_params(self, std_wl_um):
+#         yb = siepic.YBranch(pol="te")
+#         s = yb.s_params(std_wl_um)
diff --git a/tests/test_circuit.py b/tests/test_circuit.py
index b2b2826..630b284 100644
--- a/tests/test_circuit.py
+++ b/tests/test_circuit.py
@@ -14,7 +14,7 @@ except ImportError:
     JAX_AVAILABLE = False
 
 from simphony.circuit import Circuit
-from simphony.models import Model, OPort, EPort
+from simphony.models import Model, OPort, EPort, clear_name_register
 from simphony.libraries.ideal import Coupler, Waveguide
 
 
@@ -313,3 +313,21 @@ class TestMZIExample:
         wl = np.linspace(1.5, 1.6, 1000)
         res_s = ckt.s_params(wl)
         np.testing.assert_allclose(res_s, mzi_s_params)  # , atol=1e-3)
+
+
+class TestCircuitNaming:
+    def test_circuit_naming(self):
+        clear_name_register()
+        ckt = Circuit(name="MyCircuit")
+        assert ckt.name == "MyCircuit"
+
+    def test_circuit_autonaming(self):
+        clear_name_register()
+        ckt = Circuit()
+        assert ckt.name == "Circuit0"
+
+    def test_circuit_duplicate_naming(self):
+        clear_name_register()
+        ckt = Circuit(name="MyCircuit")
+        with pytest.raises(ValueError):
+            Circuit(name="MyCircuit")
diff --git a/tests/test_models.py b/tests/test_models.py
index 778393f..0584408 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -3,7 +3,14 @@ from copy import deepcopy
 import pytest
 import numpy as np
 
-from simphony.models import Model, Port, OPort, EPort
+from simphony.models import (
+    Model,
+    Port,
+    OPort,
+    EPort,
+    _NAME_REGISTER,
+    clear_name_register,
+)
 from simphony.exceptions import ModelValidationError
 from simphony.libraries.siepic import YBranch
 from simphony.libraries.ideal import Waveguide
@@ -35,14 +42,14 @@ class TestPort:
 
 class TestModelDeclaration:
     def test_missing_sparams(self):
+        """Should fail because s-params function is missing."""
         with pytest.raises(ModelValidationError):
 
             class BadModel(Model):
                 pass
 
-            BadModel()
-
     def test_missing_onames(self):
+        """Should fail because ocount or onames is missing."""
         with pytest.raises(ModelValidationError):
 
             class BadModel(Model):
@@ -61,6 +68,7 @@ class TestModelDeclaration:
             BadModel()
 
     def test_ocount_and_onames_mismatch(self):
+        """Should fail because both ocount and onames are defined."""
         with pytest.raises(ModelValidationError):
 
             class BadModel(Model):
@@ -73,6 +81,7 @@ class TestModelDeclaration:
             BadModel()
 
     def test_ocount_and_onames_length_match(self):
+        """Should fail because both ocount and onames are defined."""
         with pytest.raises(ModelValidationError):
 
             class GoodModel(Model):
@@ -301,3 +310,48 @@ class TestModelCaching:
         s2 = yb._s(tuple(std_wl_um[::-1]))
         with pytest.raises(AssertionError):
             np.testing.assert_array_equal(s1, s2)
+
+
+@pytest.fixture
+def dummy_model():
+    class MyModel(Model):
+        ocount = 2
+
+        def s_params(self, wl):
+            return wl
+
+    return MyModel
+
+
+class TestModelNaming:
+    def test_model_name(self, dummy_model):
+        clear_name_register()
+        assert dummy_model(name="Waveguide").name == "Waveguide"
+
+    def test_model_name_unique(self, dummy_model):
+        clear_name_register()
+        wg1 = dummy_model(name="Waveguide")
+        with pytest.raises(ValueError):
+            wg2 = dummy_model(name="Waveguide")
+
+    def test_auto_naming(self, dummy_model):
+        clear_name_register()
+        m1 = dummy_model()
+        m2 = dummy_model()
+        assert m1.name == "MyModel0"
+        assert m2.name == "MyModel1"
+
+    def test_auto_naming_with_name(self, dummy_model):
+        clear_name_register()
+        m1 = dummy_model(name="You can")
+        m2 = dummy_model(name="name models")
+        assert m1.name == "You can"
+        assert m2.name == "name models"
+
+    def test_auto_naming_with_name_and_auto(self, dummy_model):
+        clear_name_register()
+        m1 = dummy_model(name="You can")
+        m2 = dummy_model(name="name models")
+        m3 = dummy_model()
+        with pytest.raises(ValueError):
+            m4 = dummy_model(name="MyModel0")