{ "cells": [ { "cell_type": "markdown", "id": "eb60ace2", "metadata": {}, "source": [ "# Creating a Bill of Materials from an external data source" ] }, { "cell_type": "markdown", "id": "95c2b074", "metadata": {}, "source": [ "This example demonstrates how to use the ``bom_types`` sub-package to create a valid Granta MI XML\n", "BoM. This sub-package can be used to help construct a Granta 23/01-compliant XML BoM file to be\n", "used 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, and so the general approach can be applied to data\n", "in other formats or provided by other APIs." ] }, { "cell_type": "markdown", "id": "ad4428ef", "metadata": {}, "source": [ "You can download the external data source used in this example\n", "[here](supporting-files/source_data_sustainability.json)." ] }, { "cell_type": "markdown", "id": "97629a49", "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 further information about the\n", "expected content of XML BoMs, consult the online Granta MI documentation or your ACE\n", "representative." ] }, { "cell_type": "markdown", "id": "9f6f20aa", "metadata": {}, "source": [ "## Load the external data" ] }, { "cell_type": "markdown", "id": "dd6ce21a", "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": "30c4d98f", "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": "accac34f", "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, which identifies the type of the item.\n", "- A ``parent_part_identifier`` field, which identifies the parent part in the hierarchy.\n", "\n", "Items that refer to components do not have an equivalent record in Granta MI, and so they\n", "contain only the fields described above, and the quantity and mass field.\n", "\n", "Items that refer to materials, processes, and transport stages correspond to records in Granta MI\n", "which 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": "c6262104", "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 bill of materials 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": "123c3b8e", "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": "e339669a", "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": "2d788fb8", "metadata": {}, "outputs": [], "source": [ "source_parts = [item for item in data if item[\"type\"] == \"Part\"]\n", "source_parts[0]" ] }, { "cell_type": "markdown", "id": "6077331b", "metadata": {}, "source": [ "### Materials\n", "The third-party system only allows assignment of a single material per part, and so 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": "cede57b6", "metadata": {}, "outputs": [], "source": [ "source_materials = [item for item in data if item[\"type\"] == \"Material\"]\n", "source_materials[0]" ] }, { "cell_type": "markdown", "id": "93af069c", "metadata": {}, "source": [ "### Processes\n", "The external data source defines three different types of process:\n", "\n", "- ``MaterialFormingStep`` items describe a process which 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 will be 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 will be mapped to\n", " ``Secondary processes`` in the Granta MI BoM.\n", "- ``PartProcessingStep`` items describe processes applied directly to parts. These processes will\n", " be 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": "7afd3883", "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": "e276b9cb", "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": "168901ba", "metadata": {}, "outputs": [], "source": [ "source_joining_processes = [item for item in data if item[\"type\"] == \"PartProcessingStep\"]\n", "source_joining_processes[0]" ] }, { "cell_type": "markdown", "id": "bd164b5e", "metadata": {}, "source": [ "### Transports\n", "The external data source defines transport stages. These items of type ``Transport`` contain a\n", "``distance_in_km`` field which 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": "1adf095d", "metadata": {}, "outputs": [], "source": [ "source_transports = [item for item in data if item[\"type\"] == \"Transport\"]\n", "source_transports[0]" ] }, { "cell_type": "markdown", "id": "5a709d2e", "metadata": {}, "source": [ "## Build the BillOfMaterials object\n", "\n", "The PyGranta BoM Analytics package provides the ``bom_types`` sub-package, 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": "c1522f53", "metadata": {}, "source": [ "If you are using a customized database, change the database key value in the following cell\n", "and refer to the\n", "[Database specific configuration](3-3_Database-specific_configuration.ipynb) example to\n", "appropriately configure the connection before running any queries." ] }, { "cell_type": "code", "execution_count": null, "id": "d515f9cc", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics import bom_types\n", "DB_KEY = \"MI_Restricted_Substances\"" ] }, { "cell_type": "markdown", "id": "e25bb150", "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 will allow us to identify the correct parent part\n", "when adding materials and processes." ] }, { "cell_type": "code", "execution_count": null, "id": "33e1d28b", "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": "62a0b331", "metadata": {}, "source": [ "Next, define the hierarchy. The external data source defines a hierarchy by reference (i.e. the\n", "child part contains the identity of the parent part), but the Granta MI BoM represents the\n", "hierarchy via the BoM structure (i.e. 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": "9189bff9", "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": "bcf8dc14", "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 will\n", "be used to instantiate the required ``MIRecordReference`` objects." ] }, { "cell_type": "code", "execution_count": null, "id": "0ce43140", "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": "227d2fb7", "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 can be 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. See the\n", "online Granta MI documentation for more information about mass calculations." ] }, { "cell_type": "code", "execution_count": null, "id": "5ee5a242", "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": "4849212e", "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 this type of processes, the environmental impact is\n", "calculated based on the mass of material removed." ] }, { "cell_type": "code", "execution_count": null, "id": "ce9742c7", "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": "398ca03b", "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": "19719088", "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": "920893f1", "metadata": {}, "source": [ "### BillOfMaterials\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 cell below 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": "d09268f3", "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": "361dd806", "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": "76db1ed4", "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": "5c23f735", "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. See [Sustainability examples](../4_Sustainability/index.rst)." ] }, { "cell_type": "code", "execution_count": null, "id": "90fbd7a6", "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 }