{ "cells": [ { "cell_type": "markdown", "id": "2fc565cb", "metadata": {}, "source": [ "# Create a BoM from an external data source" ] }, { "cell_type": "markdown", "id": "9aa62212", "metadata": {}, "source": [ "This example shows how to use the ``bom_types`` subpackage to create a valid Granta MI XML\n", "BoM. This subpackage can be used to help construct a Granta 23/01-compliant XML BoM file to\n", "use with the BoM queries provided by this package. The code in this example shows how to generate\n", "a BoM from a representative JSON data source. The general approach can be applied to data\n", "in other formats or provided by other APIs." ] }, { "cell_type": "markdown", "id": "aee621f9", "metadata": {}, "source": [ "You can download the [external data source](supporting-files/source_data_sustainability.json) used in this example." ] }, { "cell_type": "markdown", "id": "9c18c59f", "metadata": {}, "source": [ "The result of this example is a Granta 23/01-compliant XML BoM file that is suitable for\n", "sustainability analysis with the Granta MI BoM Analytics API. For more information on the\n", "expected content of XML BoMs, see the Granta MI documentation." ] }, { "cell_type": "markdown", "id": "a1c77139", "metadata": {}, "source": [ "## Load the external data" ] }, { "cell_type": "markdown", "id": "118dd2eb", "metadata": {}, "source": [ "First load the JSON file and use the ``json`` module, which converts the text into a hierarchical\n", "structure of ``dict`` and ``list`` objects." ] }, { "cell_type": "code", "execution_count": null, "id": "0c9c5669", "metadata": {}, "outputs": [], "source": [ "import json\n", "from pprint import pprint\n", "\n", "with open(\"supporting-files/source_data_sustainability.json\") as f:\n", " data = json.load(f)\n", "pprint(data[:3])" ] }, { "cell_type": "markdown", "id": "0de4ceb9", "metadata": {}, "source": [ "## Inspect the external data\n", "The external data source defines a flat list of items. Each item has at least the following:\n", "\n", "- A ``type`` field that identifies the type of the item.\n", "- A ``parent_part_identifier`` field that identifies the parent part in the hierarchy.\n", "\n", "Because items that refer to components do not have an equivalent record in Granta MI, they\n", "contain only the preceding fields and quantity and mass fields.\n", "\n", "Items that refer to materials, processes, and transport stages correspond to records in Granta MI\n", "that contain the relevant sustainability metrics for these items. As a result, these items\n", "contain both a human-readable ``name`` field and a ``Granta_MI_Record_GUID`` field. In this\n", "scenario, the system that provided the data source contains the direct material assignments from\n", "Granta MI." ] }, { "cell_type": "markdown", "id": "a4e7af7c", "metadata": {}, "source": [ "### Components\n", "The external data source defines three different types of component:\n", "\n", "- A single item of type ``Product``. The external data source describes the BoM for\n", " this product. All other items are expected to be children of this item.\n", "- Items of type ``Assembly``.\n", "- Items of type ``Part``.\n", "\n", "Extract the items into separate lists based on the ``type`` field. The ``Product`` item is stored\n", "in a variable directly because there can only be one product per BoM by definition." ] }, { "cell_type": "code", "execution_count": null, "id": "847f9dea", "metadata": {}, "outputs": [], "source": [ "source_product = next(item for item in data if item[\"type\"] == \"Product\")\n", "source_product" ] }, { "cell_type": "code", "execution_count": null, "id": "6c917ab5", "metadata": {}, "outputs": [], "source": [ "source_assemblies = [item for item in data if item[\"type\"] == \"Assembly\"]\n", "source_assemblies[0]" ] }, { "cell_type": "code", "execution_count": null, "id": "818c0f4f", "metadata": {}, "outputs": [], "source": [ "source_parts = [item for item in data if item[\"type\"] == \"Part\"]\n", "source_parts[0]" ] }, { "cell_type": "markdown", "id": "52bf15e8", "metadata": {}, "source": [ "### Materials\n", "Because the third-party system only allows assignment of a single material per part, there is no\n", "'quantity' associated with the material. It is assumed that the part is made entirely of the\n", "referenced material.\n", "\n", "Extract the material items into a list based on the ``type`` field." ] }, { "cell_type": "code", "execution_count": null, "id": "490edb8b", "metadata": {}, "outputs": [], "source": [ "source_materials = [item for item in data if item[\"type\"] == \"Material\"]\n", "source_materials[0]" ] }, { "cell_type": "markdown", "id": "7227f7d3", "metadata": {}, "source": [ "### Processes\n", "The external data source defines three different types of process:\n", "\n", "- ``MaterialFormingStep`` items describe a process that forms a mass of material into a shaped\n", " component. In this scenario, the third-party system defines a single forming process for each\n", " part. These processes are mapped to ``Primary processes`` in the Granta MI BoM.\n", "- ``MaterialProcessingStep`` items describe extra processing steps applied after the main forming\n", " processing step. These items include ``step_order`` and ``mass_removed_in_kg`` fields, which\n", " together fully describe the material removal. These processes are mapped to\n", " ``Secondary processes`` in the Granta MI BoM.\n", "- ``PartProcessingStep`` items describe processes applied directly to parts. These processes are\n", " mapped to ``Joining & Finishing processes`` in the Granta MI BoM.\n", "\n", "Extract the process items into lists based on their ``type`` fields." ] }, { "cell_type": "code", "execution_count": null, "id": "2d85d2dd", "metadata": {}, "outputs": [], "source": [ "source_primary_processes = [item for item in data if item[\"type\"] == \"MaterialFormingStep\"]\n", "source_primary_processes[0]" ] }, { "cell_type": "code", "execution_count": null, "id": "5dad513a", "metadata": {}, "outputs": [], "source": [ "source_secondary_processes = [item for item in data if item[\"type\"] == \"MaterialProcessingStep\"]\n", "source_secondary_processes[0]" ] }, { "cell_type": "code", "execution_count": null, "id": "cf544154", "metadata": {}, "outputs": [], "source": [ "source_joining_processes = [item for item in data if item[\"type\"] == \"PartProcessingStep\"]\n", "source_joining_processes[0]" ] }, { "cell_type": "markdown", "id": "890778bb", "metadata": {}, "source": [ "### Transports\n", "The external data source defines transport stages. These items of type ``Transport`` contain a\n", "``distance_in_km`` field that contains the distance covered by the transport step.\n", "\n", "Extract the transport items into a list based on their ``type`` fields." ] }, { "cell_type": "code", "execution_count": null, "id": "743c8dc7", "metadata": {}, "outputs": [], "source": [ "source_transports = [item for item in data if item[\"type\"] == \"Transport\"]\n", "source_transports[0]" ] }, { "cell_type": "markdown", "id": "e9770a85", "metadata": {}, "source": [ "## Build the ``BillOfMaterials`` object\n", "\n", "The PyGranta BoM Analytics package provides the ``bom_types`` subpackage, which implements\n", "serialization and deserialization between the Granta 23/01 BoM XML schema and Python objects. This\n", "section shows how data from the external data source is processed to create BoM Python objects,\n", "which can then be serialized to an XML BoM." ] }, { "cell_type": "markdown", "id": "2fa6195c", "metadata": {}, "source": [ "If you are using a customized database, before running any queries, change the database key value\n", " in the following cell and see the\n", "[Database specific configuration](3-3_Database-specific_configuration.ipynb) example to\n", "appropriately configure the connection." ] }, { "cell_type": "code", "execution_count": null, "id": "16420d55", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics import bom_types\n", "DB_KEY = \"MI_Restricted_Substances\"" ] }, { "cell_type": "markdown", "id": "a9697efd", "metadata": {}, "source": [ "### Components\n", "\n", "The external system defines a ``part_identifier`` field that uniquely identifies parts. However,\n", "the Granta MI BoM schema requires a part to define a ``Part number``. Use the external\n", "``part_identifier`` as a part number.\n", "\n", "First, create a ``bom_types.Part`` object for every item that maps to a BoM part and add it to a\n", "dictionary indexed by the part number. This allows you to identify the correct parent part\n", "when adding materials and processes." ] }, { "cell_type": "code", "execution_count": null, "id": "7dc7473e", "metadata": {}, "outputs": [], "source": [ "components = {}\n", "\n", "# Product\n", "product_id = source_product[\"part_identifier\"]\n", "components[product_id] = bom_types.Part(\n", " part_number=product_id,\n", " quantity=bom_types.UnittedValue(\n", " value=1.0,\n", " unit=\"Each\"\n", " )\n", ")\n", "\n", "# Assemblies\n", "for item in source_assemblies:\n", " item_id = item[\"part_identifier\"]\n", " components[item_id] = bom_types.Part(\n", " part_number=item_id,\n", " quantity=bom_types.UnittedValue(\n", " value=item[\"quantity_in_parent\"],\n", " unit=\"Each\",\n", " )\n", " )\n", "\n", "# Parts\n", "for item in source_parts:\n", " item_id = item[\"part_identifier\"]\n", " components[item_id] = bom_types.Part(\n", " part_number=item_id,\n", " quantity=bom_types.UnittedValue(\n", " value=item[\"quantity_in_parent\"],\n", " unit=\"Each\",\n", " ),\n", " mass_per_unit_of_measure=bom_types.UnittedValue(\n", " value=item[\"part_mass_in_kg\"],\n", " unit=\"kg/Each\"\n", " )\n", " )\n", "\n", "print(f\"The components dict contains {len(components)} items.\")" ] }, { "cell_type": "markdown", "id": "6d611264", "metadata": {}, "source": [ "Next, define the hierarchy. The external data source defines a hierarchy by reference (for example, the\n", "child part contains the identity of the parent part), but the Granta MI BoM represents the\n", "hierarchy using the BoM structure (for example, a parent part contains all child parts as properties on the\n", "parent).\n", "\n", "The following cell iterates over all source parts and assemblies again and appends child parts to\n", "their parents' ``components`` property." ] }, { "cell_type": "code", "execution_count": null, "id": "09ec0734", "metadata": { "lines_to_next_cell": 1 }, "outputs": [], "source": [ "for item in source_assemblies + source_parts:\n", " item_id = item[\"part_identifier\"]\n", " parent_item_id = item[\"parent_part_identifier\"]\n", " item_bom_definition = components[item_id]\n", " parent_item_bom_definition = components[parent_item_id]\n", " parent_item_bom_definition.components.append(item_bom_definition)" ] }, { "cell_type": "markdown", "id": "b14c47c1", "metadata": {}, "source": [ "### Materials\n", "\n", "Next, create ``bom_types.Material`` objects for each material and add the materials to their\n", "parent part object.\n", "\n", "There are multiple possible ways of identifying Granta MI records in the BoM. In this example, the\n", "external data source holds references to Granta MI records by record GUIDs, and so the GUIDs are\n", "used to instantiate the required ``MIRecordReference`` objects." ] }, { "cell_type": "code", "execution_count": null, "id": "d50f7913", "metadata": {}, "outputs": [], "source": [ "def make_record_reference(item, db_key=DB_KEY):\n", " return bom_types.MIRecordReference(\n", " db_key=db_key,\n", " record_guid=item[\"Granta_MI_Record_GUID\"]\n", " )\n", "\n", "\n", "for item in source_materials:\n", " parent_part_id = item[\"parent_part_identifier\"]\n", " material = bom_types.Material(\n", " mi_material_reference=make_record_reference(item),\n", " identity=item[\"name\"],\n", " percentage=100.0,\n", " )\n", " components[parent_part_id].materials.append(material)" ] }, { "cell_type": "markdown", "id": "026ee284", "metadata": {}, "source": [ "### Processes\n", "\n", "In general, the order in which processes are applied is significant and can affect the result. To\n", "ensure consistency, the external system defines a ``step_order`` field, which represents the order\n", "in which processes are applied to the parent part or material. The cells in this section first\n", "sort the processes by ``step_order`` to ensure that they are added to the BoM correctly.\n", "\n", "First, apply primary and secondary processes to materials. In the external data source, the parent\n", "of a process item is always the parent part, but sustainability analysis expects only a single\n", "material assigned to each part. As a result, the process can be moved from the part to the\n", "material when constructing the Granta BoM.\n", "\n", "``MaterialFormingStep`` processes from the external data source are all mapped to ``Process`` with\n", "a ``Mass`` dimension type. This is the default value for processes whose environmental impact is\n", "calculated based on the mass of material that goes through the process. This mass is calculated\n", "from the final mass of the part and mass removed during additional processing steps. For more\n", "information on mass calculations, see the Granta MI documentation." ] }, { "cell_type": "code", "execution_count": null, "id": "d1c48402", "metadata": {}, "outputs": [], "source": [ "for item in source_primary_processes:\n", " process = bom_types.Process(\n", " mi_process_reference=make_record_reference(item),\n", " identity=item[\"name\"],\n", " dimension_type=bom_types.DimensionType.Mass,\n", " percentage=100.0\n", " )\n", " # Use the parent part identifier to retrieve the part created earlier\n", " parent_part_id = item[\"parent_part_identifier\"]\n", " # Append the process to the part via the assigned material\n", " components[parent_part_id].materials[0].processes.append(process)" ] }, { "cell_type": "markdown", "id": "2859d65a", "metadata": {}, "source": [ "Next, apply secondary processes to materials. These are added sequentially to the list of\n", "processes on the material object, in the same order as defined by the ``step_order`` field.\n", "\n", "``MaterialProcessingStep`` processes from the external data source are mapped to ``Process`` with\n", "a ``MassRemoved`` dimension type. For these processes, the environmental impact is\n", "calculated based on the mass of material removed." ] }, { "cell_type": "code", "execution_count": null, "id": "88b2e5eb", "metadata": {}, "outputs": [], "source": [ "# Sort the list of secondary processes by the ``step_order`` field.\n", "source_secondary_processes.sort(key=lambda item: (item[\"parent_part_identifier\"], item[\"step_order\"]))\n", "for item in source_secondary_processes:\n", " process = bom_types.Process(\n", " mi_process_reference=make_record_reference(item),\n", " identity=item[\"name\"],\n", " dimension_type=bom_types.DimensionType.MassRemoved,\n", " quantity=bom_types.UnittedValue(\n", " value=item[\"mass_removed_in_kg\"],\n", " unit=\"kg\",\n", " )\n", " )\n", " parent_part_id = item[\"parent_part_identifier\"]\n", " components[parent_part_id].materials[0].processes.append(process)" ] }, { "cell_type": "markdown", "id": "df73ee37", "metadata": {}, "source": [ "Finally, apply joining and finishing processes to the part.\n", "\n", "The example external data only includes part processes characterized by the length dimension.\n", "However, the Granta MI BoM schema has support for different ``DimensionType`` values depending on\n", "the process. For example, welding is typically defined by a welding path length, but a coating\n", "operation would be best quantified by an area." ] }, { "cell_type": "code", "execution_count": null, "id": "ff007e86", "metadata": {}, "outputs": [], "source": [ "unit_to_dimension_type = {\n", " \"m\": bom_types.DimensionType.Length,\n", "}\n", "\n", "source_joining_processes.sort(key=lambda item: (item[\"parent_part_identifier\"], item[\"step_order\"]))\n", "\n", "for item in source_joining_processes:\n", " process = bom_types.Process(\n", " mi_process_reference=make_record_reference(item),\n", " identity=item[\"name\"],\n", " # Map the unit in the input file to the DimensionType enum.\n", " dimension_type=unit_to_dimension_type[item[\"quantity_unit\"]],\n", " quantity=bom_types.UnittedValue(\n", " value=item[\"quantity\"],\n", " unit=item[\"quantity_unit\"]\n", " ),\n", " )\n", " parent_part_id = item[\"parent_part_identifier\"]\n", " components[parent_part_id].processes.append(process)" ] }, { "cell_type": "markdown", "id": "5415dde9", "metadata": {}, "source": [ "### ``BillOfMaterials`` object\n", "\n", "The original root part can now be retrieved from the ``components`` dictionary. This ``Part`` item\n", "contains the entire structure of parts, materials, and process objects. The following cell extracts\n", "this component from the dictionary of all components, deletes the dictionary, and prints\n", "an arbitrary property of the root component to illustrate this structure." ] }, { "cell_type": "code", "execution_count": null, "id": "8f192dd7", "metadata": {}, "outputs": [], "source": [ "root_component = components[source_product[\"part_identifier\"]]\n", "del components\n", "print(root_component.components[0].components[1].materials[0].processes[1].identity)" ] }, { "cell_type": "markdown", "id": "7b447b73", "metadata": {}, "source": [ "The final step is to create a ``BillOfMaterials`` object and add the root component and transport\n", "stages. Note that the transport stages are added to the ``BillOfMaterials`` object itself, not to\n", "a specific component." ] }, { "cell_type": "code", "execution_count": null, "id": "83ff7124", "metadata": {}, "outputs": [], "source": [ "bom = bom_types.BillOfMaterials(components=[root_component])\n", "\n", "transports = [\n", " bom_types.TransportStage(\n", " name=item[\"name\"],\n", " mi_transport_reference=make_record_reference(item),\n", " distance=bom_types.UnittedValue(value=item[\"distance_in_km\"], unit=\"km\")\n", " )\n", " for item in source_transports\n", "]\n", "bom.transport_phase = transports" ] }, { "cell_type": "markdown", "id": "d4d2f586", "metadata": {}, "source": [ "# Serialize the BoM\n", "\n", "Use the ``BomHandler`` helper class to serialize the object to XML. The resulting string can be\n", "used in a sustainability query. For more information, see the\n", "[Sustainability examples](../4_Sustainability/index.rst)." ] }, { "cell_type": "code", "execution_count": null, "id": "b55e23cc", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics import BoMHandler\n", "bom_as_xml = BoMHandler().dump_bom(bom)\n", "print(f\"{bom_as_xml[:500]}...\")" ] } ], "metadata": { "jupytext": { "formats": "ipynb,py:light" }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }