{ "cells": [ { "cell_type": "markdown", "id": "63e68a62", "metadata": {}, "source": [ "# Create an XML BoM from a CSV data source" ] }, { "cell_type": "markdown", "id": "6a8b0a6a", "metadata": {}, "source": [ "This example shows how to use the ``bom_types`` subpackage to create a Granta MI BoM.\n", "This subpackage can be used to help construct a Granta 24/12-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 CSV data source. The general approach can be applied to data\n", "in other formats or provided by other APIs." ] }, { "cell_type": "markdown", "id": "91d56aac", "metadata": {}, "source": [ "You can download the [CSV file](../supporting-files/glass_door.csv) used in this example." ] }, { "cell_type": "markdown", "id": "19b15783", "metadata": {}, "source": [ "The result of this example is a Granta 24/12-compliant XML BoM file that is suitable for\n", "compliance or 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": "2a448b08", "metadata": {}, "source": [ "## Load the external data" ] }, { "cell_type": "markdown", "id": "bb236fb5", "metadata": {}, "source": [ "First load the CSV file and use ``pandas`` to load the content in a dataframe." ] }, { "cell_type": "code", "execution_count": null, "id": "40fed359", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "df = pd.read_csv(\"../supporting-files/glass_door.csv\")\n", "df.head()" ] }, { "cell_type": "markdown", "id": "47431496", "metadata": {}, "source": [ "## Inspect the external data\n", "The CSV file describes the bill of materials for the door assembly introduced in the\n", "[Creating an XML BoM](./6-1_Creating_an_XML_BoM.ipynb) example.\n", "The door (24X6-30) contains two hinges (HA-42-Al) and a handle (H-S-BR-Dual), both fixed to the panel (P-30-L) with\n", "machine screws (DIN-7991-M8-20) and washers (N0403.12N.2). The door glass (321-51) is coated with a partially\n", "reflective polymer film (7000001298).\n", "\n", "The hierarchy of items within the BoM is defined by the order of items in the CSV and the ``BoM Level`` column. If an\n", "item has the BoM level *n*, then the item's parent is the first level preceding it in the CSV with a BoM level *n-1*.\n", "\n", "Each item includes an ``Item Type`` column that identifies the type of the item, in this example the only values are\n", "``Part`` or ``Material``.\n", "Additional columns are specific to the type of item.\n", "\n", "### Part items\n", "\n", "Items that refer to parts only exist in the BoM and do not reference records in Granta MI. Their ``Name`` and ``ID``\n", "are defined only in the BoM to identify parts.\n", "\n", "The ``Quantity`` and ``Unit of measure`` columns describe the quantity of part expected in the parent. This can be\n", "done by specifying how many occurrences of a part are in an assembly, or for example for the glass door, the surface\n", "area of laminated glass.\n", "\n", "There are three types of components in this example BoM:\n", "\n", " - The product described by the BoM, whose ``BoM Level`` is ``1``.\n", " - Assemblies which are made of sub-parts.\n", " - Their mass isn't defined in the BoM because it can be computed by rolling up the mass of the sub-parts.\n", " - Parts which are defined by their mass and the material they are made of.\n", " - Their mass is defined via the ``Measured mass (per UoM)`` and ``Measured mass unit``. The units for the mass\n", " must be consistent with the unit used to define the quantity of the part, so that when the quantity is\n", " multiplied with the mass per UoM, it resolves to a mass. For example, the quantity of glass panel included in\n", " the door is defined as a surface area ``1.51 m^2``, which requires the mass per unit of measure to be defined\n", " as a mass per surface area ``19.6 kg/m^2``.\n", "\n", "For more information on the different options available for specifying part quantities and part masses, see the\n", "Granta MI documentation.\n", "\n", "### Material items\n", "\n", "Items that refer to materials correspond to records in Granta MI that contain the relevant compliance or\n", "sustainability information for these items. As a result, these items include both a human-readable ``Name`` column and\n", "an ``ID`` column. In this scenario, the system that provided the data source contains the Granta MI material\n", "assignments for each part based on the ``Material ID`` attribute, which is included in the ``ID`` column.\n", "\n", "Materials are described in terms of percentage of the parent part made of the material." ] }, { "cell_type": "markdown", "id": "f8f9e03e", "metadata": {}, "source": [ "## Build the ``BillsOfMaterials`` object\n", "\n", "Import the ``eco2412`` sub-package and helper classes to build record and attribute references." ] }, { "cell_type": "code", "execution_count": null, "id": "f22c16e6", "metadata": { "lines_to_end_of_cell_marker": 0, "lines_to_next_cell": 1 }, "outputs": [], "source": [ "from ansys.grantami.bomanalytics.bom_types import eco2412, AttributeReferenceBuilder, RecordReferenceBuilder\n", "\n", "DB_KEY = \"MI_Restricted_Substances\"\n", "TABLE_NAME = \"MaterialUniverse\"" ] }, { "cell_type": "markdown", "id": "5e7ae4e4", "metadata": {}, "source": [ "Define a function that accepts a part row as input and returns a ``eco2412.Part`` object." ] }, { "cell_type": "code", "execution_count": null, "id": "d18dee33", "metadata": { "lines_to_next_cell": 1 }, "outputs": [], "source": [ "def make_part(item: pd.Series) -> eco2412.Part:\n", " mass = item[\"Measured mass (per UoM)\"]\n", " if not pd.isna(mass):\n", " mass_per_unit_of_measure = eco2412.UnittedValue(\n", " value=mass,\n", " unit=item[\"Measured mass unit\"],\n", " )\n", " else:\n", " mass_per_unit_of_measure = None\n", " return eco2412.Part(\n", " part_number=item[\"ID\"],\n", " part_name=item[\"Name\"],\n", " quantity=eco2412.UnittedValue(value=item[\"Quantity\"], unit=item[\"Unit of measure\"]),\n", " mass_per_unit_of_measure=mass_per_unit_of_measure,\n", " )" ] }, { "cell_type": "markdown", "id": "745d358a", "metadata": {}, "source": [ "Define a function that accepts a material row as input and returns a ``eco2412.Material`` object. Materials are\n", "identified by their attribute value ``Material ID``, so the record reference is defined using a lookup value." ] }, { "cell_type": "code", "execution_count": null, "id": "9889ecbc", "metadata": {}, "outputs": [], "source": [ "material_id_reference = (\n", " AttributeReferenceBuilder(DB_KEY)\n", " .with_attribute_name(\"Material ID\")\n", " .with_table_name(TABLE_NAME)\n", " .build()\n", ")\n", "\n", "def make_material(item: pd.Series) -> eco2412.Material:\n", " unit = item[\"Unit of measure\"]\n", " if unit == \"%\":\n", " percentage = item[\"Quantity\"]\n", " else:\n", " raise ValueError(\"Method 'make_material' only supports quantities defined as percentages.\")\n", "\n", " material_reference = (\n", " RecordReferenceBuilder(db_key=DB_KEY)\n", " .with_lookup_value(lookup_value=item[\"ID\"], lookup_attribute_reference=material_id_reference)\n", " .build()\n", " )\n", " return eco2412.Material(\n", " mi_material_reference=material_reference,\n", " identity=item[\"ID\"],\n", " name=item[\"Name\"],\n", " percentage=percentage,\n", " )" ] }, { "cell_type": "markdown", "id": "49444f14", "metadata": {}, "source": [ "Iterate over the rows in the CSV file and convert to ``bom_types`` objects. Because items only ever appear after\n", "their parent, a list is used to keep track of possible parent items in the BoM." ] }, { "cell_type": "code", "execution_count": null, "id": "9b055336", "metadata": {}, "outputs": [], "source": [ "# Instantiate the hierarchy with an empty BoM object\n", "bom = eco2412.BillOfMaterials(components=[])\n", "path = [bom]" ] }, { "cell_type": "code", "execution_count": null, "id": "5c002b6e", "metadata": {}, "outputs": [], "source": [ "for _, item_row in df.iterrows():\n", " item_level = item_row[\"BoM Level\"]\n", " parent = path[item_level - 1]\n", "\n", " item_type = item_row[\"Item Type\"]\n", " if item_type == \"Part\":\n", " item = make_part(item_row)\n", " parent.components.append(item)\n", " elif item_type == \"Material\":\n", " item = make_material(item_row)\n", " parent.materials.append(item)\n", " else:\n", " raise ValueError(f\"Unsupported 'Item Type': '{item_type}'\")\n", "\n", " # Update the hierarchy with the newly created item\n", " path = path[:item_level] + [item]" ] }, { "cell_type": "markdown", "id": "ce842dc5", "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 BoM query." ] }, { "cell_type": "code", "execution_count": null, "id": "21415dff", "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 }