pymats 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. pymats-0.1.0/PKG-INFO +55 -0
  2. pymats-0.1.0/README.md +46 -0
  3. pymats-0.1.0/pymats/cae/__init__.py +42 -0
  4. pymats-0.1.0/pymats/cae/constitutive.py +127 -0
  5. pymats-0.1.0/pymats/cae/stressstraincurve.py +156 -0
  6. pymats-0.1.0/pymats/data/__init__.py +7 -0
  7. pymats-0.1.0/pymats/data/collection.py +100 -0
  8. pymats-0.1.0/pymats/data/loader.py +325 -0
  9. pymats-0.1.0/pymats/data/mechanicalproperty/__init__.py +98 -0
  10. pymats-0.1.0/pymats/data/mechanicalproperty/approximationmodel.py +123 -0
  11. pymats-0.1.0/pymats/data/mechanicalproperty/property.py +104 -0
  12. pymats-0.1.0/pymats/materials/__init__.py +4 -0
  13. pymats-0.1.0/pymats/materials/asmematerial.py +85 -0
  14. pymats-0.1.0/pymats/materials/constants.py +71 -0
  15. pymats-0.1.0/pymats/materials/custommaterials.py +31 -0
  16. pymats-0.1.0/pymats/materials/enmaterial.py +69 -0
  17. pymats-0.1.0/pymats/materials/materialbase.py +516 -0
  18. pymats-0.1.0/pymats/pymats.egg-info/PKG-INFO +55 -0
  19. pymats-0.1.0/pymats/pymats.egg-info/SOURCES.txt +39 -0
  20. pymats-0.1.0/pymats/pymats.egg-info/dependency_links.txt +1 -0
  21. pymats-0.1.0/pymats/pymats.egg-info/top_level.txt +4 -0
  22. pymats-0.1.0/pymats/utils/__init__.py +38 -0
  23. pymats-0.1.0/pymats/utils/allowables.py +122 -0
  24. pymats-0.1.0/pymats/utils/composition.py +71 -0
  25. pymats-0.1.0/pymats/utils/designpoint.py +20 -0
  26. pymats-0.1.0/pymats/utils/log.py +47 -0
  27. pymats-0.1.0/pymats/utils/mbasemodel.py +16 -0
  28. pymats-0.1.0/pymats/utils/myjsonencoder.py +59 -0
  29. pymats-0.1.0/pymats/utils/pma/builder.py +36 -0
  30. pymats-0.1.0/pymats/utils/pma/document.py +37 -0
  31. pymats-0.1.0/pymats/utils/pma/renderer.py +121 -0
  32. pymats-0.1.0/pymats/utils/testing.py +7 -0
  33. pymats-0.1.0/pymats.egg-info/PKG-INFO +40 -0
  34. pymats-0.1.0/pymats.egg-info/SOURCES.txt +10 -0
  35. pymats-0.1.0/pymats.egg-info/dependency_links.txt +1 -0
  36. pymats-0.1.0/pymats.egg-info/top_level.txt +1 -0
  37. pymats-0.1.0/pyproject.toml +19 -0
  38. pymats-0.1.0/setup.cfg +4 -0
  39. pymats-0.1.0/tests/test_config.py +0 -0
  40. pymats-0.1.0/tests/test_materials.py +17 -0
  41. pymats-0.1.0/tests/test_module.py +9 -0
pymats-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymats
3
+ Version: 0.1.0
4
+ Summary: A python module for handling materials
5
+ Author-email: Sebastian Mau <klixz09@gmail.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
10
+ # PyMats
11
+ PyMats is a material database app for Python.
12
+ It is designed to help you manage and organize your materials and data.
13
+ You can create, edit, and delete materials, as well as search for materials by name or ID.
14
+ PyMats also allows you to copy materials from other collections and save them to your own collection.
15
+ The api can be used to load material data to extternal applications, like excel. In additon the pymats module
16
+ can be used in python directly to interact with the database.
17
+
18
+ ## Main Actions
19
+ - Search for materials by name or ID
20
+ - Copy materials from other collections
21
+ - Save materials to your own collection
22
+ - Load material data to external applications
23
+ - Interact with the database using the pymats module
24
+ - Get CAE Input Files for Ansys, LS-Dyna and Abaqus
25
+ - Compare Materials and find equivalents
26
+ - Generate Particular Material Apraisals (PMAs) automatically with custom requirements
27
+ - Since Excel is widley used, you can create a customized excel data table with your materials
28
+ - Future: "Engineering Data" for Ansys Workbench
29
+
30
+ The public collections and its materials are free to use. The material data is built from regression models
31
+ and only **approximate** the data of the corresponding standards.
32
+
33
+ ## Getting started
34
+ ### Python Module
35
+ #### Installation
36
+ The python module is available with `pip install pymats`
37
+ #### Example
38
+ ```python
39
+ from pymats import Material`
40
+ material = Material(Idenitifier="SA-723-2-2")`
41
+ ```
42
+ This will load the material via the api by default. See the configuration section for defining custom databases or data sources.
43
+
44
+ ## Using the API
45
+ The general endpoints `/load_materials`, `/material/{mid}/` and `/get_mids` can just be used like any other api endpoints. Check the [docu](/) for information of returns. If an enpoint requires authentication, provide your personal remote token as `?user=<token>`. The token can be placed in the env variable `$REMOTE_TOKEN` or your credentials are stored in `~/.pymats/credentials.json`.
46
+
47
+ ## Using in Excel
48
+ With the token you have access to an excel addin based on OfficeJS. You can also use the token with a vba function. The addin can be downloaded via the [download section](/)
49
+
50
+ ## Using in Mathcad
51
+ With the token you can download a compiled dll you can include to your mathcad environment to get the related udf's.
52
+
53
+ ## Configuration
54
+ ### Environment Variables
55
+ `$PYMATS_TOKEN`: used for authentication
pymats-0.1.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # PyMats
2
+ PyMats is a material database app for Python.
3
+ It is designed to help you manage and organize your materials and data.
4
+ You can create, edit, and delete materials, as well as search for materials by name or ID.
5
+ PyMats also allows you to copy materials from other collections and save them to your own collection.
6
+ The api can be used to load material data to extternal applications, like excel. In additon the pymats module
7
+ can be used in python directly to interact with the database.
8
+
9
+ ## Main Actions
10
+ - Search for materials by name or ID
11
+ - Copy materials from other collections
12
+ - Save materials to your own collection
13
+ - Load material data to external applications
14
+ - Interact with the database using the pymats module
15
+ - Get CAE Input Files for Ansys, LS-Dyna and Abaqus
16
+ - Compare Materials and find equivalents
17
+ - Generate Particular Material Apraisals (PMAs) automatically with custom requirements
18
+ - Since Excel is widley used, you can create a customized excel data table with your materials
19
+ - Future: "Engineering Data" for Ansys Workbench
20
+
21
+ The public collections and its materials are free to use. The material data is built from regression models
22
+ and only **approximate** the data of the corresponding standards.
23
+
24
+ ## Getting started
25
+ ### Python Module
26
+ #### Installation
27
+ The python module is available with `pip install pymats`
28
+ #### Example
29
+ ```python
30
+ from pymats import Material`
31
+ material = Material(Idenitifier="SA-723-2-2")`
32
+ ```
33
+ This will load the material via the api by default. See the configuration section for defining custom databases or data sources.
34
+
35
+ ## Using the API
36
+ The general endpoints `/load_materials`, `/material/{mid}/` and `/get_mids` can just be used like any other api endpoints. Check the [docu](/) for information of returns. If an enpoint requires authentication, provide your personal remote token as `?user=<token>`. The token can be placed in the env variable `$REMOTE_TOKEN` or your credentials are stored in `~/.pymats/credentials.json`.
37
+
38
+ ## Using in Excel
39
+ With the token you have access to an excel addin based on OfficeJS. You can also use the token with a vba function. The addin can be downloaded via the [download section](/)
40
+
41
+ ## Using in Mathcad
42
+ With the token you can download a compiled dll you can include to your mathcad environment to get the related udf's.
43
+
44
+ ## Configuration
45
+ ### Environment Variables
46
+ `$PYMATS_TOKEN`: used for authentication
@@ -0,0 +1,42 @@
1
+ import numpy as np
2
+ from typing import Optional, Literal
3
+ from pymats import MaterialBase
4
+ from pymats.cae.constitutive import ConstitutiveModel, IsotropicModel
5
+ from pymats.cae.stressstraincurve import StressStrainCurve, ModelConstants
6
+
7
+ class CAEModel:
8
+
9
+ material: MaterialBase
10
+
11
+ def __init__(self, material: MaterialBase) -> None:
12
+ self.material = material
13
+
14
+
15
+ def get_stress_strain_curve(self, approach: ModelConstants.Approaches) -> StressStrainCurve:
16
+ a_s = self.material.ElongationBreak.long if self.material.ElongationBreak else 0.12
17
+ if np.isclose(a_s, 0.0):
18
+ a_s = 1.2
19
+ curve = StressStrainCurve(
20
+ Model=approach,
21
+ youngs_modulus=self.material.EO,
22
+ yield_strength=self.material.SYo,
23
+ tensile_strength=self.material.STo,
24
+ elongation_at_break=a_s
25
+ )
26
+ curve.attach_material(self.material)
27
+ curve.build()
28
+ return curve
29
+
30
+
31
+ def get_constitutive_model(
32
+ self, d: Optional[np.ndarray] = None,
33
+ kinematics: Literal['full', 'axissymmetric', 'plane strain', 'plane stress'] = 'full'
34
+ ) -> ConstitutiveModel:
35
+ return IsotropicModel(
36
+ d or IsotropicModel.compute_d(
37
+ self.material.EO,
38
+ self.material.PoissonsRatio,
39
+ kinematics
40
+ ),
41
+ self.material.EO, self.material.PoissonsRatio
42
+ )
@@ -0,0 +1,127 @@
1
+ from typing import Literal
2
+ import numpy as np
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class Plasticity:
7
+
8
+ tangent_modulus: float = 203e3
9
+ @property
10
+ def H(self):
11
+ return self.tangent_modulus
12
+
13
+ def __init__(self, e: float) -> None:
14
+ # initalized with tangent modulus as youngs modulus for no plasticity
15
+ self.tangent_modulus = e
16
+
17
+
18
+
19
+ class ConstitutiveModel(ABC):
20
+
21
+ plasticity: Plasticity
22
+
23
+ _d: np.ndarray
24
+ @property
25
+ def D(self):
26
+ return self._d
27
+
28
+ @classmethod
29
+ @abstractmethod
30
+ def compute_d(cls, *args, **kwargs) -> np.ndarray:
31
+ pass
32
+
33
+ def __init__(self, d: np.ndarray, e: float, nu: float) -> None:
34
+ super().__init__()
35
+ self._d = d
36
+ self.plasticity = Plasticity(e)
37
+
38
+ def compute_elastic_stress(self, eps: np.ndarray):
39
+ return self._d @ eps
40
+
41
+ def set_plastiticity(
42
+ self,
43
+ plasticity: Literal['linear hardening' ,'perfect'],
44
+ sy: float,
45
+ st: float,
46
+ e: float,
47
+ a_s: float
48
+ ):
49
+ if plasticity == 'perfect':
50
+ self.plasticity.tangent_modulus = 0.0
51
+ elif plasticity == 'linear hardening':
52
+ eps_el = sy / e
53
+ eps_pl = a_s - eps_el
54
+ ds = st - sy
55
+ self.plasticity.tangent_modulus = ds / eps_pl
56
+
57
+ class IsotropicModel(ConstitutiveModel):
58
+
59
+ # lame parameters
60
+ mu: float
61
+ lam: float
62
+ # ##################
63
+
64
+
65
+ @classmethod
66
+ def compute_lame_parameters(cls, e, nu):
67
+ mu = e / (2 * (1 + nu))
68
+ lam = e * nu / ((1 + nu) * (1 - 2 * nu))
69
+ return mu, lam
70
+
71
+ @classmethod
72
+ def compute_axissymmetric_stiffness(cls, e: float, nu: float):
73
+ c = e / ((1 + nu) * (1 - 2 * nu))
74
+ return c * np.array([
75
+ [1 - nu, nu, nu, 0],
76
+ [nu, 1 - nu, nu, 0],
77
+ [nu, nu, 1 - nu, 0],
78
+ [0, 0, 0, (1 - 2 * nu) / 2]
79
+ ])
80
+
81
+ @classmethod
82
+ def compute_plane_strain_stiffness(cls, e: float, nu: float):
83
+ c = e / ((1 + nu) * (1 - 2 * nu))
84
+ return c * np.array([
85
+ [1 - nu, nu, 0],
86
+ [nu, 1 - nu, 0],
87
+ [0, 0, (1 - 2 * nu) / 2.0]
88
+ ])
89
+
90
+ @classmethod
91
+ def compute_plane_stress_stiffness(cls, e: float, nu: float):
92
+ c = e / (1 - nu **2)
93
+ return c * np.array([
94
+ [1, nu, 0],
95
+ [nu, 1, 0],
96
+ [0, 0, (1 - nu) / 2]
97
+ ])
98
+
99
+ @classmethod
100
+ def compute_full_stiffness(cls, e: float, nu: float):
101
+ mu, lam = cls.compute_lame_parameters(e, nu)
102
+ d = np.zeros((6, 6))
103
+ d[:3, :3] = lam
104
+ np.fill_diagonal(d[:3, :3], lam + 2 * mu)
105
+ d[3:, 3:] = np.eye(3) * mu
106
+ return d
107
+
108
+ @classmethod
109
+ def compute_d(cls, e: float, nu: float, kinematics: Literal['full', 'plane strain', 'plane stress', 'axissymmetric']) -> np.ndarray:
110
+
111
+ if kinematics == 'axissymmetric':
112
+ d = cls.compute_axissymmetric_stiffness(e, nu)
113
+ elif kinematics == 'plane strain':
114
+ d = cls.compute_plane_strain_stiffness(e, nu)
115
+ elif kinematics == 'plane stress':
116
+ d = cls.compute_plane_stress_stiffness(e, nu)
117
+ elif kinematics == 'full':
118
+ d = cls.compute_full_stiffness(e, nu)
119
+ else:
120
+ raise ValueError("Don't understand kinematic requirement!")
121
+ return d
122
+
123
+
124
+ def __init__(self, d: np.ndarray, e: float, nu: float) -> None:
125
+ super().__init__(d, e, nu)
126
+ self.mu, self.lam = self.compute_lame_parameters(e, nu)
127
+
@@ -0,0 +1,156 @@
1
+ import numpy as np
2
+ from typing import Optional
3
+
4
+ from pydantic import computed_field, PrivateAttr
5
+
6
+ from pymats.utils.log import logger
7
+ from enum import Enum
8
+ from pymats.utils.mbasemodel import MBaseModel
9
+ from pymats import MaterialBase
10
+
11
+
12
+ class ModelConstants:
13
+ class Approaches(str, Enum):
14
+ LINEARELASTIC = "linearelastic"
15
+ PERFECTPLASTICITY = "perfectplastic"
16
+ LINEARHARDENING = "linearhardening"
17
+
18
+ class StressStrainCurve(MBaseModel):
19
+
20
+ _data: np.ndarray = PrivateAttr(default=np.array((2, 2)))
21
+ _material: MaterialBase | None = PrivateAttr(default=None)
22
+ @computed_field(return_type=np.ndarray)
23
+ @property
24
+ def Data(self):
25
+ return self._data
26
+ Model: ModelConstants.Approaches
27
+
28
+ # material properties
29
+ youngs_modulus: float
30
+ yield_strength: float
31
+ tensile_strength: float
32
+ elongation_at_break: float
33
+
34
+ def attach_material(self, material: MaterialBase) -> None:
35
+ self._material = material
36
+
37
+ def get_elastic_strain(self) -> float:
38
+ if not self._material:
39
+ raise ValueError("Material not attached to stress-strain curve.")
40
+ if not np.isclose(self._material.EO, 0.0):
41
+ return self._material.SYo / self._material.EO
42
+ raise ZeroDivisionError("The Young's modulus is zero! Check material data.")
43
+
44
+ def get_ansys_model(self) -> str:
45
+ if not self._material:
46
+ raise ValueError("Material not attached to stress-strain curve.")
47
+ lines = [
48
+ "/PREP7",
49
+ f"! Material {self._material.Synopsis} properties",
50
+ f"MP, EX, {self._material.MID}, {self._material.EO}",
51
+ f"MP, PRXY, {self._material.MID}, {self._material.PoissonsRatio}",
52
+ f"MP, DENS, {self._material.MID}, {self._material.Density}",
53
+ ]
54
+
55
+ if self.Model in [ModelConstants.Approaches.LINEARHARDENING, ModelConstants.Approaches.PERFECTPLASTICITY]:
56
+ hardening_modulus = 0.0
57
+ if self.Model == ModelConstants.Approaches.LINEARHARDENING:
58
+ a_s = self._material.ElongationBreak.long if self._material.ElongationBreak else 0.12
59
+ eps_el = self.get_elastic_strain()
60
+ hardening_modulus = (self._material.STo - self._material.SYo) / (a_s - eps_el)
61
+
62
+ lines.extend([
63
+ "! Plasticity with isotropic hardening",
64
+ f"TB, PLASTIC, {self._material.MID}, 1, 1",
65
+ f"TBPT, 1, {self._material.SYo}, {hardening_modulus}"
66
+ ])
67
+
68
+ lines.append("/FINISH")
69
+ return "\n".join(lines)
70
+
71
+ def get_dyna_model(self) -> str:
72
+ if not self._material:
73
+ raise ValueError("Material not attached to stress-strain curve.")
74
+ lines = ["*KEYWORD"]
75
+ args = ["MID", "RO", "E", "PR"]
76
+ values = [self._material.MID, self._material.Density, self._material.EO, self._material.PoissonsRatio]
77
+
78
+ if self.Model == ModelConstants.Approaches.LINEARELASTIC:
79
+ lines.append("*MAT_ELASTIC")
80
+ else:
81
+ lines.append("*MAT_PIECEWISE_LINEAR_PLASTICITY")
82
+ args.extend(["SIGY", "ETAN", "FAIL", "TDEL"])
83
+ values.extend([self._material.SYo, 0.0, 0.0, 0.0])
84
+
85
+ lines.append("$ " + "\t".join(args))
86
+ lines.append("\t".join(map(str, values)))
87
+
88
+ if self.Model != ModelConstants.Approaches.LINEARELASTIC:
89
+ hardening_stress = self._material.STo if self.Model == ModelConstants.Approaches.LINEARHARDENING else self._material.SYo
90
+ lines.extend([
91
+ "$ Epsilon-Plastic Strain vs. Stress Data",
92
+ "$ EPS\tSIG",
93
+ f"0.0\t{self._material.SYo}",
94
+ f"{self._material.ElongationBreak}\t{hardening_stress}"
95
+ ])
96
+
97
+ lines.append("*END")
98
+ return "\n".join(lines)
99
+
100
+ def get_abaqus_model(self) -> str:
101
+ if not self._material:
102
+ raise ValueError("Material not attached to stress-strain curve.")
103
+ lines = [
104
+ f"*Material, NAME={self._material.Synopsis}",
105
+ "*DENSITY",
106
+ f"{self._material.Density}",
107
+ "*ELASTIC",
108
+ f"{self._material.EO}, {self._material.PoissonsRatio}"
109
+ ]
110
+
111
+ if self.Model != ModelConstants.Approaches.LINEARELASTIC:
112
+ lines.extend([
113
+ "*PLASTIC",
114
+ f"{self._material.SYo}, 0.0",
115
+ f"{self._material.STo}, {self._material.ElongationBreak}"
116
+ ])
117
+
118
+ return "\n".join(lines)
119
+
120
+ def _build_linear_elastic(self):
121
+ self._data = np.array([
122
+ [0, 0],
123
+ [self.yield_strength / self.youngs_modulus, self.yield_strength]
124
+ ])
125
+
126
+ def _build_perfect_plasticity(self):
127
+ self._data = np.array([
128
+ [0, 0],
129
+ [self.yield_strength / self.youngs_modulus, self.yield_strength],
130
+ [0.3, self.yield_strength]
131
+ ])
132
+
133
+ def _build_linear_plasticity(self):
134
+ self._data = np.array([
135
+ [0, 0],
136
+ [self.yield_strength / self.youngs_modulus, self.yield_strength],
137
+ [self.elongation_at_break, self.tensile_strength]
138
+ ])
139
+
140
+ def add_material_properties(self, *args):
141
+ self.youngs_modulus, self.yield_strength, self.tensile_strength, self.elongation_at_break = args
142
+
143
+ def reset(self):
144
+ self._data = np.zeros((2, 2))
145
+
146
+ def build(self):
147
+ try:
148
+ if self.Model == ModelConstants.Approaches.LINEARELASTIC:
149
+ self._build_linear_elastic()
150
+ elif self.Model == ModelConstants.Approaches.PERFECTPLASTICITY:
151
+ self._build_perfect_plasticity()
152
+ elif self.Model == ModelConstants.Approaches.LINEARHARDENING:
153
+ self._build_linear_plasticity()
154
+ except ZeroDivisionError:
155
+ logger.warning("Division by zero error!")
156
+ return
@@ -0,0 +1,7 @@
1
+ from typing import List
2
+ from pymats.data.loader import LoaderFactory, MaterialIndex
3
+
4
+
5
+ def get_mids(query: str | None = None) -> List[MaterialIndex]:
6
+ loader = LoaderFactory.get_loader()
7
+ return loader.get_identifiers(query=query)
@@ -0,0 +1,100 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from typing import List, Union, Set, Type
4
+ from sklearn.metrics.pairwise import cosine_similarity
5
+
6
+ from pydantic import PrivateAttr, computed_field, SerializeAsAny
7
+
8
+ from pymats.materials.asmematerial import ASMEMaterial
9
+ from pymats.materials.enmaterial import ENMaterial
10
+ from pymats.materials.materialbase import MaterialBase
11
+ from pymats.materials.constants import MaterialDataConstants
12
+ from pymats.data.loader import MaterialLoaderBase
13
+ from pymats import Material, MaterialFactory
14
+ from pymats.utils.log import logger
15
+ from pymats.config import settings
16
+ from pymats.utils.mbasemodel import MBaseModel
17
+
18
+ class MaterialCollection(MBaseModel):
19
+
20
+ CollectionName: str
21
+ IsCustomCollection: bool = False
22
+ materials: list[SerializeAsAny[MaterialBase]] = []
23
+
24
+ @classmethod
25
+ def load_from_db(
26
+ cls,
27
+ handler: MaterialLoaderBase,
28
+ collection_name: str,
29
+ identifiers: list[str] = [],
30
+ include_property_data: bool = True
31
+ ) -> "MaterialCollection":
32
+
33
+ is_custom = collection_name in settings.CustomCollectionNames
34
+
35
+ docs = handler.load_collection_materials(
36
+ collection_name,
37
+ material_names=identifiers,
38
+ include_property_data=include_property_data
39
+ )
40
+ materials = []
41
+ for doc in docs.values():
42
+ try:
43
+ materials.append(
44
+ MaterialFactory.from_data(doc, init_mech_properties=False)
45
+ )
46
+ except Exception as ex:
47
+ name = doc.get('Name', 'none')
48
+ logger.warning(f'Failed to load material {str(name)} for collection {str(collection_name)} with {str(ex)}')
49
+ continue
50
+ return cls(CollectionName=collection_name, IsCustomCollection=is_custom, materials=materials)
51
+
52
+ @computed_field(return_type=list[str])
53
+ @property
54
+ def Keys(self):
55
+ if len(self.materials) > 0:
56
+ class_t = self.materials[0].__class__
57
+ else:
58
+ class_t = Material
59
+ return list(class_t.model_fields.keys())
60
+
61
+
62
+ def is_standard_collection(self) -> bool:
63
+ """
64
+ Check if the collection is a standard collection (ASME, EN).
65
+
66
+ Returns:
67
+ bool: True if the collection is a standard collection, False otherwise.
68
+ """
69
+ return self.CollectionName in MaterialDataConstants.Collections.__members__.values()
70
+
71
+ def find_equivalents(self, material: MaterialBase, tolerance: float = 0.0):
72
+ master = material.tokenize(20 ,0)
73
+ for mat in self.materials:
74
+ try:
75
+ if mat.Identifier == material.Identifier:
76
+ continue
77
+ candidate = mat.tokenize(20, 0)
78
+ sim = cosine_similarity(master.reshape(1, -1), candidate.reshape(1, -1))
79
+ # euclidean = np.linalg.norm(master.reshape(1, -1) - candidate.reshape(1, -1))
80
+ if np.max(sim) >= tolerance:
81
+ yield (mat, np.max(sim))
82
+ except Exception as e:
83
+ logger.error(f"Failed to compare materials: {e}")
84
+ continue
85
+
86
+ def get_equivalents(self, material: MaterialBase, tolerance: float = 0.0):
87
+ equivalents = []
88
+ for mat in self.find_equivalents(material, tolerance):
89
+ material, sim = mat
90
+ equivalents.append({
91
+ "material": material.Synopsis,
92
+ "standard": material.Standards,
93
+ "score": sim
94
+ })
95
+ df = pd.DataFrame(equivalents)
96
+ df.sort_values(by="score", ascending=False, inplace=True)
97
+ df.reset_index(drop=True, inplace=True)
98
+ df = df.iloc[:10, :]
99
+ return df.to_dict(orient="records")
100
+