Download this example as a Jupyter notebook or a Python script.

Determining Compliance for BoMs in External Data Sources¶

Introduction¶

You may have to deal with Bills of Materials or other data structures stored in third-party systems. This example shows a scenario where compliance needs to be determined for a BoM-type structure in a JSON file, with the result added to the input file.

Although it is unlikely that the data structures and processing presented here will be an exact match for your requirements, this example is intended to demonstrate the principles behind using the BoM Analytics API within your existing processes. It shows how a BoM-like data structure can be loaded from a neutral format and used as a starting point for compliance analysis. The approach is applicable to data in other formats, or data loaded from other software platform APIs.

The external data source used in this example can be downloaded here.

Load the External Data¶

First load the JSON file and use the json module to convert the text into a hierarchical structure of dict and list objects.

[1]:

import json
from pprint import pprint

with open("supporting-files/source_data.json") as f:
pprint(data)

{'components': [{'materials': ['plastic-abs-pvc-flame'],
'part_number': 'FQC3RQKYE5'},
{'materials': ['plastic-abs-pvc-flame',
'plastic-pc-10glassfiber'],
'part_number': '3FF8CXIHWJ'},
{'materials': ['elastomer-pvc-shorea55'],
'part_number': 'O676WZSGHA'},
{'materials': ['aluminum-6063-t6'],
'part_number': 'L8NTU4BZY2'},
{'materials': ['aluminum-6063-t6'],
'part_number': '7N9EUBALU2'},
{'materials': ['stainless-316-annealed'],
'part_number': 'U921IQSW6K'},
{'materials': ['aluminum-6063-t6', 'glass-borosilicate-7050'],
'part_number': 'TUSMQ1ZDFM'}]}


The list of components will be used frequently, so we store this in a variable for convenience.

[2]:

components = data["components"]


It is clear from viewing this data that some parts include multiple materials, and some materials appear in the JSON file more than once. However, the material compliance is not dependent on the component it is used in, and the compliance of a part only depends on the worst compliance status of the constituent materials. Therefore we can simplify the compliance query by get the compliance for the unique set of materials in the JSON file, and perform some data manipulation of the results.

Since the compliance status of a material does not depend on which component it is used in, and part compliance depends only on the worst compliance status of its constituent materials, we can simplify the query by running it against the set of unique materials in the JSON file. We can then rebuild the data structure from these results to view the compliance by component.

First, use a set comprehension to get the unique materials, which we can then cast into a list.

[3]:

material_ids = {m for comp in components for m in comp["materials"]}
material_ids

[3]:

{'aluminum-6063-t6',
'elastomer-pvc-shorea55',
'glass-borosilicate-7050',
'plastic-abs-pvc-flame',
'plastic-pc-10glassfiber',
'stainless-316-annealed'}


Getting the Compliance Status¶

Next, create and run a compliance query using the list of material IDs, as shown in previous exercises.

[4]:

from ansys.grantami.bomanalytics import Connection, indicators, queries

server_url = "http://my_grantami_server/mi_servicelayer"
cxn = Connection(server_url).with_credentials("user_name", "password").connect()
svhc = indicators.WatchListIndicator(
name="SVHC",
legislation_names=["REACH - The Candidate List"],
default_threshold_percentage=0.1,
)
mat_query = (
queries.MaterialComplianceQuery()
.with_indicators([svhc])
.with_material_ids(material_ids)
)
mat_results = cxn.run(mat_query)
mat_results

[4]:

<MaterialComplianceQueryResult: 6 MaterialWithCompliance results>


Post-Processing the Results¶

The results above describe the compliance status for each material, but more work is needed to provide the compliance status for all the components in the original JSON.

When a component contains only one material, the result can simply be copied over. In the general case, moving from material compliance to component compliance means taking the worst compliance result across all the constituent materials.

To do this, first create a dictionary that maps a material ID to the indicator result returned by the query.

[5]:

material_lookup = {mat.material_id: mat.indicators["SVHC"]
for mat in mat_results.compliance_by_material_and_indicator}


Next, define a function that takes a list of material IDs and returns the worst compliance status associated with the materials in the list.

We can use the built-in max() function to do this, because WatchListIndicator objects can be compared with > and < operators. The convention is that a worse result is ‘greater than’ a better result.

[6]:

def rollup_results(material_ids) -> str:
indicator_results = [material_lookup[mat_id] for mat_id in material_ids]
worst_result = max(indicator_results)
return worst_result.flag.name


Now call this function for each component in a dict comprehension to obtain a mapping between part number and compliance status.

[7]:

component_results = {comp["part_number"]: rollup_results(comp["materials"])
for comp in components}
component_results

[7]:

{'FQC3RQKYE5': 'WatchListHasSubstanceAboveThreshold',
'3FF8CXIHWJ': 'WatchListHasSubstanceAboveThreshold',
'O676WZSGHA': 'WatchListHasSubstanceAboveThreshold',
'L8NTU4BZY2': 'WatchListCompliant',
'7N9EUBALU2': 'WatchListCompliant',
'U921IQSW6K': 'WatchListCompliant',
'TUSMQ1ZDFM': 'WatchListHasSubstanceAboveThreshold'}


These results include text defined by the API for compliance status. However, we may want the compliance status to determine the approvals required to release the part in a design review process. In that case, we can define a mapping between compliance status and approval requirements.

[8]:

flags = indicators.WatchListFlag
result_map = {
flags.WatchListCompliant.name: "No Approval Required",
flags.WatchListAllSubstancesBelowThreshold.name: "Level 1 Approval Required",
flags.WatchListHasSubstanceAboveThreshold.name: "Level 2 Approval Required",
}


We can now use this dictionary to map from the Granta MI result to the approval requirements.

[9]:

results = {part_number: result_map[result]
for part_number, result in component_results.items()}
results

[9]:

{'FQC3RQKYE5': 'Level 2 Approval Required',
'3FF8CXIHWJ': 'Level 2 Approval Required',
'O676WZSGHA': 'Level 2 Approval Required',
'L8NTU4BZY2': 'No Approval Required',
'7N9EUBALU2': 'No Approval Required',
'U921IQSW6K': 'No Approval Required',
'TUSMQ1ZDFM': 'Level 2 Approval Required'}


Write the Output¶

Once we have our final result, we can take our result dict and use it to extend the original JSON data structure, with approval requirements added in.

[10]:

components_with_result = []
for component in components:
component_with_result = component
part_number = component["part_number"]
component_with_result["approval"] = results[part_number]
components_with_result.append(component_with_result)

data_results = {}
data_results["components"] = components_with_result


Printing the results shows the new data structure with the results included.

[11]:

pprint(data_results)

{'components': [{'approval': 'Level 2 Approval Required',
'materials': ['plastic-abs-pvc-flame'],
'part_number': 'FQC3RQKYE5'},
{'approval': 'Level 2 Approval Required',
'materials': ['plastic-abs-pvc-flame',
'plastic-pc-10glassfiber'],
'part_number': '3FF8CXIHWJ'},
{'approval': 'Level 2 Approval Required',
'materials': ['elastomer-pvc-shorea55'],
'part_number': 'O676WZSGHA'},
{'approval': 'No Approval Required',
'materials': ['aluminum-6063-t6'],
'part_number': 'L8NTU4BZY2'},
{'approval': 'No Approval Required',
'materials': ['aluminum-6063-t6'],
'part_number': '7N9EUBALU2'},
{'approval': 'No Approval Required',
'materials': ['stainless-316-annealed'],
'part_number': 'U921IQSW6K'},
{'approval': 'Level 2 Approval Required',
'materials': ['aluminum-6063-t6', 'glass-borosilicate-7050'],
'part_number': 'TUSMQ1ZDFM'}]}