Skip to content
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

Overhaul json parsing approach #76

Open
lukethehuman opened this issue Oct 8, 2024 · 1 comment · May be fixed by #91
Open

Overhaul json parsing approach #76

lukethehuman opened this issue Oct 8, 2024 · 1 comment · May be fixed by #91

Comments

@lukethehuman
Copy link
Collaborator

lukethehuman commented Oct 8, 2024

Currently, we maintain a mapping between various classnames and the actual names of the classes they point to. For example, "pin" maps to the class PinAssembly. These mappings are used in various places to determine which class to instantiate.

Suggest we replace this functionality with a function which returns the subclass by its actual name (here, PinAssembly). This would be much easier to maintain and read, as well as improving extensibility.

The function might look something like this:

def get_subclass_from_classname(classname) -> Assembly | Component:
    """Return an Assembly or Component subclass by name."""
    base_classes = [Assembly, Component]
    for bc in base_classes:
        for sc in bc.__subclasses__():
            if sc.__name__ == classname:
                return subclass
    raise ValueError("No such class.")

Note with this change, we would then use the proper class name in the json files "class": "PinAssembly" instead of "class": "pin". This would be a breaking change, but should lead to a simplier interface in the end.

Note that this should also pick up any user-defined subclasses which would also be in scope. See issue #66.

I suggest we only retain the current lower case classnames for use in the identifiers. See issue #75. (We could use the CamelCase classnames without issue, but the consideration for identifiers should be more about how assembly and component names should appear in the resulting exported files.)

@lukethehuman
Copy link
Collaborator Author

I have had a go at implementing this in the lhumph-experimental branch. I ended up with an Assembly __init__ which looks like this:

    def __init__(
        self,
        materials: Materials | dict | None = None,
        geometry: Geometry | dict | None = None,
        components: dict[
            str, dict[str, Materials | Geometry | dict | None]
        ] | None = None,
    ):
        self.identifier = self.name
        self.materials = materials
        self.geometry = geometry
        self.components = components

    @classmethod
    def make_dataclasses(cls, param_dict) -> dict:
        """Recursively search the parameter dictionary for classes with
        associated materials & geometry dataclasses and instantiate them within
        the dictionary. For parameter dictionaries without materials or
        geometry keys, these are added with the value None.
        """
        # Convert material parameters for the assembly.
        if "materials" not in param_dict:
            param_dict["materials"] = None
        elif hasattr(cls, "materials_dataclass"):
            materials_dict = param_dict["materials"]
            param_dict["materials"] = cls.materials_dataclass(**materials_dict)

        # Convert geometry parameters for the assembly.
        if "geometry" not in param_dict:
            param_dict["geometry"] = None
        elif hasattr(cls, "geometry_dataclass"):
            geometry_dict = param_dict["geometry"]
            param_dict["geometry"] = cls.geometry_dataclass(**geometry_dict)

        # Recursively convert any components.
        for (name, component_param_dict) in param_dict["components"].items():
            classname = component_param_dict["class"]
            component_subcls = get_subclass_from_classname(classname)
            param_dict["components"][name] = component_subcls.make_dataclasses(
                component_param_dict
            )

        return param_dict

    @classmethod
    def load_nested_jsons(cls, param_dict) -> dict:
        """Recursively search the components section of a parameter dictionary
        for references to json files and load them in. Return the full nested
        parameter dictionary.

        Parameters
        ----------
        param_dict : dict
            The assembly's parameter dictionary to resolve.

        Returns
        -------
        param_dict : dict
            The resolved parameter dictionary.
        """
        if "components" not in param_dict:
            return param_dict
        for (name, component_param_dict) in param_dict["components"].items():
            if isinstance(component_param_dict, str):
                component_param_dict = load_json(component_param_dict)
            param_dict["components"][name] = cls.load_nested_jsons(
                component_param_dict
            )
        return param_dict

    @classmethod
    def from_dict(cls, param_dict: dict) -> Self:
        """Instantiate an Assembly subclass by parsing a parameter dictionary.

        Parameters
        ----------
        param_dict : dict
            Dictionary describing which Assembly subclass to instantiate and
            the required materials, geometry, and component parameters.

        Returns
        -------
        assembly : Assembly
            An instance of the Assembly subclass described by param_dict.
        """
        classname = param_dict["class"]
        subcls = get_subclass_from_classname(classname)
        param_dict = cls.load_nested_jsons(param_dict)
        param_dict = subcls.make_dataclasses(param_dict)
        materials = param_dict["materials"]
        geometry = param_dict["geometry"]
        components = param_dict["components"]
        assembly = subcls(materials, geometry, components)
        return assembly

    @classmethod
    def from_json(cls, json_file: str) -> Self:
        """Instantiate an Assembly subclass by parsing a json file.

        Parameters
        ----------
        json_file : str
            Path to the json file describing which Assembly subclass to
            instantiate and the required materials, geometry, and component
            parameters.

        Returns
        -------
        assembly : Assembly
            An instance of the Assembly subclass described by param_dict.
        """
        param_dict = load_json(json_file)
        return cls.from_dict(param_dict)

@sid-mungale sid-mungale linked a pull request Nov 14, 2024 that will close this issue
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 a pull request may close this issue.

1 participant