{ "cells": [ { "cell_type": "markdown", "id": "775d891b", "metadata": {}, "source": [ "# Create an XML BoM using Python" ] }, { "cell_type": "markdown", "id": "85aabaad", "metadata": {}, "source": [ "The `bom_types` namespace can be used in conjunction with the `BoMHandler` class to\n", "create and manipulate BoMs for analysis. This example demonstrates creating a BoM for\n", "a laminated glass door and then uses the BoM as the input to a compliance query." ] }, { "cell_type": "markdown", "id": "f1933b30", "metadata": {}, "source": [ "The door contains two hinges and a handle, both fixed to the frame with machine screws and\n", "washers, the door glass is coated with a partially reflective polymer film." ] }, { "cell_type": "markdown", "id": "f442d5ed", "metadata": {}, "source": [ "Most Granta MI deployments will use the default database key and table names:" ] }, { "cell_type": "code", "execution_count": null, "id": "4a237435", "metadata": {}, "outputs": [], "source": [ "DB_KEY = \"MI_Restricted_Substances\"\n", "TABLE_NAME = \"MaterialUniverse\"" ] }, { "cell_type": "markdown", "id": "3407a08a", "metadata": {}, "source": [ "## Create the Bill of Materials as Python objects" ] }, { "cell_type": "markdown", "id": "d0092a01", "metadata": {}, "source": [ "The structure of an XML BoM is hierarchical, individual parts belong to assemblies which can\n", "belong to larger assemblies. It is possible to construct the BoM in one statement, but this\n", "example uses the recommended approach of building each part up from objects that represent\n", "smaller sub-assemblies." ] }, { "cell_type": "markdown", "id": "62d7c6ea", "metadata": {}, "source": [ "It is possible to have the same part in multiple places in the BoM, so we define two helper functions\n", "to create a copy of a part and set the quantity." ] }, { "cell_type": "code", "execution_count": null, "id": "33663651", "metadata": { "lines_to_end_of_cell_marker": 0, "lines_to_next_cell": 1 }, "outputs": [], "source": [ "import copy\n", "\n", "from ansys.grantami.bomanalytics.bom_types.eco2412 import BillOfMaterials, Material, Part, UnittedValue\n", "from ansys.grantami.bomanalytics.bom_types.gbt1205 import MIRecordReference\n", "\n", "def add_part_to_assembly_with_count(child: Part, count: int) -> Part:\n", " return add_part_to_assembly_with_quantity(child, float(count), \"Each\")\n", "\n", "def add_part_to_assembly_with_quantity(child: Part, quantity: float, unit: str) -> Part:\n", " assigned_part = copy.deepcopy(child)\n", " assigned_part.quantity = UnittedValue(quantity, unit=unit)\n", " return assigned_part" ] }, { "cell_type": "markdown", "id": "4c8ae4b2", "metadata": {}, "source": [ "Material references define abstract references to Granta MI records, and so can be reused.\n", "Material references can be defined in different ways." ] }, { "cell_type": "markdown", "id": "d95c22ec", "metadata": {}, "source": [ "The following references are created using different types of GUID. Record GUIDs identify a\n", "specific version of the record, while Record History GUIDs identify the latest accessible\n", "version of the record." ] }, { "cell_type": "code", "execution_count": null, "id": "b36213b3", "metadata": {}, "outputs": [], "source": [ "laminated_glass_reference = MIRecordReference(db_key=DB_KEY, record_guid=\"85ed8b21-c2e6-4c43-8ec3-4c12a44c820c\")\n", "hardened_stainless_reference = MIRecordReference(db_key=DB_KEY, record_guid=\"fcc49a93-6b92-4751-9b85-f00b7769190d\")\n", "nylon_pa6_reference = MIRecordReference(db_key=DB_KEY, record_history_guid=\"1c7884dd-80ed-4661-89d6-4b6e56a08ed7\")" ] }, { "cell_type": "markdown", "id": "13c41503", "metadata": {}, "source": [ "Some databases also have unique identifiers for materials. If these are Short Text attributes\n", "they can be used as lookup values, for example in MaterialUniverse we can use the \"Material ID\"\n", "attribute." ] }, { "cell_type": "code", "execution_count": null, "id": "1b8dd2f2", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics.bom_types import AttributeReferenceBuilder\n", "\n", "material_id_reference = (AttributeReferenceBuilder(DB_KEY)\n", " .with_attribute_name(\"Material ID\")\n", " .with_table_name(TABLE_NAME)\n", " .build())\n", "\n", "pet_film_reference = MIRecordReference(\n", " db_key=DB_KEY,\n", " lookup_attribute_reference=material_id_reference,\n", " lookup_value=\"plastic-pet\"\n", ")\n", "annealed_stainless_reference = MIRecordReference(\n", " db_key=DB_KEY,\n", " lookup_attribute_reference=material_id_reference,\n", " lookup_value=\"stainless-304-annealed\"\n", ")\n", "aluminium_319_reference = MIRecordReference(\n", " db_key=DB_KEY,\n", " lookup_attribute_reference=material_id_reference,\n", " lookup_value=\"aluminum-319-0-moldcast-t6\"\n", ")\n", "steel_1015_reference = MIRecordReference(\n", " db_key=DB_KEY,\n", " lookup_attribute_reference=material_id_reference,\n", " lookup_value=\"steel-1015-normalized\"\n", ")" ] }, { "cell_type": "markdown", "id": "b5f2a0a2", "metadata": {}, "source": [ "Nylon washers exist in multiple parts, so define these first. The part number has no effect\n", "on the analysis and simply identifies each part in the result." ] }, { "cell_type": "code", "execution_count": null, "id": "f79ec154", "metadata": {}, "outputs": [], "source": [ "washer_part = Part(\n", " part_number=\"N0403.12N.2\",\n", " mass_per_unit_of_measure=UnittedValue(2., \"g/Part\"),\n", " materials=[Material(mi_material_reference=nylon_pa6_reference, percentage=100.)]\n", ")" ] }, { "cell_type": "markdown", "id": "35a2fa5e", "metadata": {}, "source": [ "Start with sub-assemblies and assemble the BoM.\n", "The hinge assembly consists of two casting parts, four washers, and two machine screws" ] }, { "cell_type": "code", "execution_count": null, "id": "d6a80b68", "metadata": {}, "outputs": [], "source": [ "hinge_casting_a = Part(\n", " part_number=\"HA-42-Al(A)\",\n", " mass_per_unit_of_measure=UnittedValue(146., \"g/Part\"),\n", " materials=[Material(mi_material_reference=aluminium_319_reference, percentage=100.)]\n", ")\n", "\n", "hinge_casting_b = Part(\n", " part_number=\"HA-42-Al(B)\",\n", " mass_per_unit_of_measure=UnittedValue(220., \"g/Part\"),\n", " materials=[Material(mi_material_reference=aluminium_319_reference, percentage=100.)]\n", ")\n", "\n", "machine_screw_part = Part(\n", " part_number=\"DIN-7991-M8-20\",\n", " mass_per_unit_of_measure=UnittedValue(8.6, \"g/Part\"),\n", " materials=[Material(mi_material_reference=hardened_stainless_reference, percentage=100.)]\n", ")\n", "\n", "hinge_assembly = Part(\n", " part_number=\"HA-42-Al\",\n", " components=[\n", " add_part_to_assembly_with_count(hinge_casting_a, 1),\n", " add_part_to_assembly_with_count(hinge_casting_b, 1),\n", " add_part_to_assembly_with_count(washer_part, 4),\n", " add_part_to_assembly_with_count(machine_screw_part, 2),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "id": "03b80fa9", "metadata": {}, "source": [ "The handle assembly consists of two stainless steel handles, two mild steel pins, four\n", "nylon washers and a pair of grub screws. The pin screws into part B and is retained in\n", "part A with a grub screw." ] }, { "cell_type": "code", "execution_count": null, "id": "ca43b321", "metadata": {}, "outputs": [], "source": [ "handle_part_a = Part(\n", " part_number=\"H-S-BR-A\",\n", " mass_per_unit_of_measure=UnittedValue(472., \"g/Part\"),\n", " materials=[Material(mi_material_reference=annealed_stainless_reference, percentage=100.)]\n", ")\n", "\n", "handle_part_b = Part(\n", " part_number=\"H-S-BR-B\",\n", " mass_per_unit_of_measure=UnittedValue(464., \"g/Part\"),\n", " materials=[Material(mi_material_reference=annealed_stainless_reference, percentage=100.)]\n", ")\n", "\n", "handle_pin_part = Part(\n", " part_number=\"H-PIN-12\",\n", " mass_per_unit_of_measure=UnittedValue(46.5, \"g/Part\"),\n", " materials=[Material(mi_material_reference=steel_1015_reference, percentage=100.)]\n", ")\n", "\n", "handle_grub_screw_part = Part(\n", " part_number=\"SSF-M4-6-A2\",\n", " mass_per_unit_of_measure=UnittedValue(1.3, \"g/Part\"),\n", " materials=[Material(mi_material_reference=hardened_stainless_reference, percentage=100.)]\n", ")\n", "\n", "handle_assembly = Part(\n", " part_number=\"H-S-BR-Dual\",\n", " components=[\n", " add_part_to_assembly_with_count(handle_part_a, 1),\n", " add_part_to_assembly_with_count(handle_part_b, 1),\n", " add_part_to_assembly_with_count(handle_pin_part, 2),\n", " add_part_to_assembly_with_count(washer_part, 4),\n", " add_part_to_assembly_with_count(handle_grub_screw_part, 2)\n", " ]\n", ")" ] }, { "cell_type": "markdown", "id": "761fb81d", "metadata": {}, "source": [ "The glass panel consists of a laminated glass door and a layer of PET solar control\n", "film." ] }, { "cell_type": "code", "execution_count": null, "id": "dcad3af7", "metadata": {}, "outputs": [], "source": [ "glass_panel = Part(\n", " part_number=\"321-51\",\n", " mass_per_unit_of_measure=UnittedValue(19.6, \"kg/m^2\"),\n", " materials=[Material(mi_material_reference=laminated_glass_reference, percentage=100.)])\n", "\n", "solar_control_film = Part(\n", " part_number=\"7000001298\",\n", " mass_per_unit_of_measure=UnittedValue(340., \"g/m^2\"),\n", " materials=[Material(mi_material_reference=pet_film_reference, percentage=100.)]\n", ")\n", "\n", "panel_assembly = Part(\n", " part_number=\"P-30-L\",\n", " components=[\n", " add_part_to_assembly_with_quantity(glass_panel, 1.51, \"m^2\"),\n", " add_part_to_assembly_with_quantity(solar_control_film, 1.51, \"m^2\"),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "id": "a2e11849", "metadata": {}, "source": [ "The whole door assembly is then a combination of two hinges, one handle assembly and one\n", "door panel." ] }, { "cell_type": "code", "execution_count": null, "id": "ab72a07f", "metadata": {}, "outputs": [], "source": [ "door_assembly = Part(\n", " part_number=\"24X6-30\",\n", " components=[\n", " add_part_to_assembly_with_count(hinge_assembly, 2),\n", " add_part_to_assembly_with_count(handle_assembly, 1),\n", " add_part_to_assembly_with_count(panel_assembly, 1),\n", " ]\n", ")" ] }, { "cell_type": "markdown", "id": "302b38f6", "metadata": {}, "source": [ "## Serialize the BoM to XML" ] }, { "cell_type": "markdown", "id": "51028050", "metadata": {}, "source": [ "Generate a BoM from the door assembly part, and serialize the BoM to XML." ] }, { "cell_type": "code", "execution_count": null, "id": "3034b101", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics import BoMHandler\n", "\n", "door_assembly_bom = BillOfMaterials(components=[door_assembly])\n", "bom_handler = BoMHandler()\n", "\n", "rendered_bom = bom_handler.dump_bom(door_assembly_bom)\n", "rendered_bom.splitlines()[0:10]" ] }, { "cell_type": "markdown", "id": "12bd4354", "metadata": {}, "source": [ "## Run a compliance query" ] }, { "cell_type": "markdown", "id": "00e9df25", "metadata": {}, "source": [ "Now that you have created an XML BoM, run a compliance query to determine whether the BoM complies\n", "with a specific legislation.\n", "First, connect to Granta MI." ] }, { "cell_type": "code", "execution_count": null, "id": "c767638d", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics import Connection\n", "\n", "server_url = \"http://my_grantami_server/mi_servicelayer\"\n", "cxn = Connection(server_url).with_credentials(\"user_name\", \"password\").connect()" ] }, { "cell_type": "markdown", "id": "f5eecca0", "metadata": {}, "source": [ "The compliance BoM query accepts a single XML BoM as a string and one or more indicators. In\n", "this case we perform a query against the SIN list, using the RoHS indicator with a threshold\n", "of 0.1%." ] }, { "cell_type": "code", "execution_count": null, "id": "c62f49f1", "metadata": {}, "outputs": [], "source": [ "from ansys.grantami.bomanalytics import indicators, queries\n", "\n", "sin_list = indicators.WatchListIndicator(\n", " name=\"EU REACH Candidate List\",\n", " legislation_ids=[\"Candidate_AnnexXV\"],\n", " default_threshold_percentage=0.1,\n", ")\n", "\n", "compliance_query = (\n", " queries.BomComplianceQuery()\n", " .with_bom(rendered_bom)\n", " .with_indicators([sin_list])\n", ")\n", "\n", "compliance_result = cxn.run(compliance_query)\n", "compliance_result" ] }, { "cell_type": "markdown", "id": "40608fcd", "metadata": {}, "source": [ "The ``BomComplianceQueryResult`` object returned after running the compliance query contains a list of\n", "``PartWithComplianceResult`` objects.\n", "The following cell prints the compliance status of the BoM." ] }, { "cell_type": "code", "execution_count": null, "id": "7f0fd023", "metadata": {}, "outputs": [], "source": [ "root_part = compliance_result.compliance_by_part_and_indicator[0]\n", "print(f\"BoM Compliance Status: {root_part.indicators['EU REACH Candidate List'].flag.name}\")" ] } ], "metadata": { "jupytext": { "formats": "ipynb,py:light" }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }