pyroll-phase-transformation-model 3.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 (23) hide show
  1. pyroll_phase_transformation_model-3.1.0/.gitignore +152 -0
  2. pyroll_phase_transformation_model-3.1.0/LICENSE +11 -0
  3. pyroll_phase_transformation_model-3.1.0/PKG-INFO +32 -0
  4. pyroll_phase_transformation_model-3.1.0/hatch.toml +32 -0
  5. pyroll_phase_transformation_model-3.1.0/pyproject.toml +48 -0
  6. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/__init__.py +43 -0
  7. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/microstructure.py +275 -0
  8. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/__init__.py +6 -0
  9. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/bainite.py +29 -0
  10. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/base.py +52 -0
  11. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/diffusional_phase_transformation.py +248 -0
  12. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/ferrite.py +29 -0
  13. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/martensite.py +51 -0
  14. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/perlite.py +29 -0
  15. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/profile.py +165 -0
  16. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/__init__.py +3 -0
  17. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/sigmoidal_function_base.py +89 -0
  18. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/sigmoidal_function_i.py +18 -0
  19. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/sigmoidal_function_s.py +18 -0
  20. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/__init__.py +8 -0
  21. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/hardness.py +44 -0
  22. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/mechanical_properties.py +65 -0
  23. pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/transformation_solver.py +341 -0
@@ -0,0 +1,152 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105
+ __pypackages__/
106
+
107
+ # Celery stuff
108
+ celerybeat-schedule
109
+ celerybeat.pid
110
+
111
+ # SageMath parsed files
112
+ *.sage.py
113
+
114
+ # Environments
115
+ .env
116
+ .venv
117
+ env/
118
+ venv/
119
+ ENV/
120
+ env.bak/
121
+ venv.bak/
122
+
123
+ # Spyder project settings
124
+ .spyderproject
125
+ .spyproject
126
+
127
+ # Rope project settings
128
+ .ropeproject
129
+
130
+ # mkdocs documentation
131
+ /site
132
+
133
+ # mypy
134
+ .mypy_cache/
135
+ .dmypy.json
136
+ dmypy.json
137
+
138
+ # Pyre type checker
139
+ .pyre/
140
+
141
+ # pytype static type analyzer
142
+ .pytype/
143
+
144
+ # Cython debug symbols
145
+ cython_debug/
146
+
147
+ # PyCharm
148
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
149
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
151
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152
+ .idea/
@@ -0,0 +1,11 @@
1
+ Copyright 2026 Christoph Renzing
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyroll-phase-transformation-model
3
+ Version: 3.1.0
4
+ Summary: PyRolL plugin computing the austenite-decomposition phase composition (and resulting hardness/mechanical properties) along the reconstructed thermal history of a pass sequence, including a ring-wise distribution.
5
+ Project-URL: Homepage, https://pyroll.readthedocs.io
6
+ Project-URL: Repository, https://github.com/ChRen95/pyroll-phase-transformation-modele
7
+ Project-URL: Documentation, https://github.com/ChRen95/pyroll-phase-transformation-model/blob/main/docs/docs.pdf
8
+ Author-email: Christoph Renzing <christophrenzing@posteo.de>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Framework :: Hatch
13
+ Classifier: Framework :: IPython
14
+ Classifier: Framework :: Jupyter
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Framework :: Sphinx
17
+ Classifier: Intended Audience :: Manufacturing
18
+ Classifier: Intended Audience :: Science/Research
19
+ Classifier: License :: OSI Approved :: BSD License
20
+ Classifier: Natural Language :: English
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.9
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Topic :: Scientific/Engineering
27
+ Requires-Python: <4.0,>=3.9
28
+ Requires-Dist: pandas
29
+ Requires-Dist: pyroll-core
30
+ Requires-Dist: pyroll-jmak-recrystallization
31
+ Requires-Dist: pyroll-ring-model-thermal
32
+ Requires-Dist: scipy
@@ -0,0 +1,32 @@
1
+ [build]
2
+ packages = [
3
+ "pyroll" # place your package in the pyroll namespace
4
+ ]
5
+
6
+ [version]
7
+ path = "pyroll/phase_transformation_model/__init__.py"
8
+
9
+ [envs.default]
10
+ path = ".venv"
11
+ dependencies = [
12
+ "pytest ~= 7.0",
13
+ "pyroll-report"
14
+ ]
15
+
16
+ [envs.docs]
17
+ path = ""
18
+ dependencies = [
19
+ "pygments ~= 2.0"
20
+ ]
21
+
22
+ [envs.docs.scripts]
23
+ build = "latexmk --cd --pdf --shell-escape docs/docs" # compile LaTeX docs with latexmk
24
+
25
+ [envs.test]
26
+ path = ""
27
+
28
+ [envs.test.scripts]
29
+ all = "pytest"
30
+
31
+ [[envs.test.matrix]]
32
+ python = ["3.9", "3.10", "3.11"]
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "pyroll-phase-transformation-model"
3
+ description = "PyRolL plugin computing the austenite-decomposition phase composition (and resulting hardness/mechanical properties) along the reconstructed thermal history of a pass sequence, including a ring-wise distribution."
4
+ authors = [
5
+ { name = "Christoph Renzing", email = "christophrenzing@posteo.de" }
6
+ ]
7
+ license = "BSD-3-Clause"
8
+ requires-python = ">=3.9, <4.0"
9
+
10
+ dependencies = [
11
+ "pyroll-core",
12
+ "pyroll-ring-model-thermal",
13
+ "pyroll-jmak-recrystallization",
14
+ "scipy",
15
+ "pandas",
16
+ ]
17
+
18
+ classifiers = [
19
+ "Intended Audience :: Science/Research",
20
+ "Intended Audience :: Manufacturing",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Scientific/Engineering",
23
+ "License :: OSI Approved :: BSD License",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Development Status :: 5 - Production/Stable",
29
+ "Framework :: Hatch",
30
+ "Framework :: IPython",
31
+ "Framework :: Jupyter",
32
+ "Framework :: Pytest",
33
+ "Framework :: Sphinx",
34
+ "Natural Language :: English",
35
+ ]
36
+
37
+ dynamic = ["version"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://pyroll.readthedocs.io"
41
+
42
+ # change these URLs to your needs
43
+ Repository = "https://github.com/ChRen95/pyroll-phase-transformation-modele"
44
+ Documentation = "https://github.com/ChRen95/pyroll-phase-transformation-model/blob/main/docs/docs.pdf"
45
+
46
+ [build-system]
47
+ requires = ["hatchling"]
48
+ build-backend = "hatchling.build"
@@ -0,0 +1,43 @@
1
+ VERSION = "3.1.0"
2
+
3
+ from . import profile # noqa: F401 (registers profile hooks)
4
+ from . import microstructure # noqa: F401 (registers result hooks)
5
+
6
+ from .sigmoidal_functions import S, I
7
+ from .phase_transformations import (
8
+ Martensite,
9
+ Bainite,
10
+ Ferrite,
11
+ Perlite,
12
+ DiffusionalPhaseTransformation,
13
+ )
14
+ from .transformation import (
15
+ PhaseTransformationSolver,
16
+ vickers_hardness_martensite,
17
+ vickers_hardness_bainite,
18
+ vickers_hardness_ferrite_perlite,
19
+ tensile_strength_from_hardness,
20
+ elongation_a50,
21
+ reduction_of_area,
22
+ impact_energy_ku,
23
+ )
24
+ from .microstructure import Config
25
+
26
+ __all__ = [
27
+ "S",
28
+ "I",
29
+ "Martensite",
30
+ "Bainite",
31
+ "Ferrite",
32
+ "Perlite",
33
+ "DiffusionalPhaseTransformation",
34
+ "PhaseTransformationSolver",
35
+ "vickers_hardness_martensite",
36
+ "vickers_hardness_bainite",
37
+ "vickers_hardness_ferrite_perlite",
38
+ "tensile_strength_from_hardness",
39
+ "elongation_a50",
40
+ "reduction_of_area",
41
+ "impact_energy_ku",
42
+ "Config",
43
+ ]
@@ -0,0 +1,275 @@
1
+ """Integration of the phase-transformation model into the PyRoll hook system.
2
+
3
+ The standalone :class:`~.transformation.PhaseTransformationSolver` works on a
4
+ thermal cycle ``T(t)``. Inside a PyRoll process that cycle is not given
5
+ explicitly; it has to be reconstructed from the solved unit tree. As the bar
6
+ flows through the sequence, every unit's out-profile carries the accumulated
7
+ time :pyattr:`~pyroll.core.Profile.t` and its :pyattr:`temperature`. Cooling
8
+ units (:class:`~pyroll.core.Transport`, :class:`~pyroll.core.CoolingPipe`,
9
+ :class:`~pyroll.core.Spooler`) are :class:`~pyroll.core.DiskElementUnit`\\ s, so
10
+ their disk elements provide the fine-grained ``(t, T)`` samples that resolve the
11
+ cooling rates the kinetics depend on.
12
+
13
+ This module walks that tree, assembles the thermal history up to and including
14
+ any out-profile, and exposes the resulting microstructure as profile hooks:
15
+
16
+ * :pyattr:`microstructure_composition` – bulk phase fractions (core hook).
17
+ * :pyattr:`ring_microstructure_compositions` – per-ring phase fractions, using
18
+ the ``ring_temperatures`` of :mod:`pyroll.ring_model_thermal`.
19
+ * :pyattr:`vickers_hardness` – overall Maynier hardness (core hook).
20
+ * :pyattr:`mechanical_properties` – tensile strength, elongation, etc.
21
+ * :pyattr:`thermal_history` / :pyattr:`ring_thermal_histories` – the
22
+ reconstructed ``T(t)`` cycles, mainly for inspection.
23
+
24
+ All result hooks are **lazy**: they are computed (and cached) only when accessed,
25
+ e.g. ``sequence[i].out_profile.microstructure_composition``. They are
26
+ intentionally not registered as root hooks, because root hooks are re-evaluated
27
+ on every convergence iteration of every unit, which would make the per-step
28
+ solver prohibitively expensive. To have them appear in reports, add the desired
29
+ hook to ``pyroll.core.root_hooks`` in your own configuration.
30
+ """
31
+
32
+ import numpy as np
33
+ import pandas as pd
34
+
35
+ from pyroll.core import Unit, Hook, config
36
+
37
+ from .transformation import PhaseTransformationSolver
38
+
39
+ #: Phase columns produced by the solver, in report order.
40
+ _PHASES = ("ferrite", "perlite", "bainite", "martensite", "austenite")
41
+
42
+ #: Key under which the computed solver frame is cached on a profile instance.
43
+ _FRAME_CACHE = "_ptm_frame"
44
+
45
+
46
+ @config("PYROLL_PHASE_TRANSFORMATION_MODEL")
47
+ class Config:
48
+ THERMAL_HISTORY_SAMPLES = 1000
49
+ """Number of uniform time samples the thermal history is resampled to
50
+ before it is handed to the kinetics solver. The solver assumes a uniform
51
+ time grid, while the reconstructed history is non-uniform; this is the grid
52
+ it is interpolated onto. Higher values are more accurate but slower."""
53
+
54
+
55
+ @Unit.OutProfile.extension_class
56
+ class PhaseTransformationOutProfile(Unit.OutProfile):
57
+ """Result hooks added to every unit out-profile."""
58
+
59
+ thermal_history = Hook[dict]()
60
+ """The reconstructed scalar thermal cycle up to this profile as a dict with
61
+ the keys ``t`` (time in s) and ``T`` (temperature in K), each a 1-D array."""
62
+
63
+ ring_thermal_histories = Hook[np.ndarray]()
64
+ """The reconstructed per-ring thermal cycles up to this profile as a 2-D
65
+ array of shape ``(n_samples, n_rings)`` aligned with
66
+ :pyattr:`~pyroll.ring_model.RingProfile.rings`. Available only if ring
67
+ temperatures are present (``pyroll-ring-model-thermal`` in use)."""
68
+
69
+ ring_microstructure_compositions = Hook[np.ndarray]()
70
+ """Per-ring phase composition: an object array of ``{phase: fraction}``
71
+ dicts, one per ring from core to surface. Available only if ring
72
+ temperatures are present."""
73
+
74
+ mechanical_properties = Hook[dict]()
75
+ """Estimated mechanical properties derived from the final microstructure
76
+ (``HV``, ``ym``, ``Rm``, ``A50``, ``Z``, ``KU``)."""
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Thermal-history reconstruction (plain helpers, return None when unavailable)
81
+ # ---------------------------------------------------------------------------
82
+
83
+ def _root_unit(unit: Unit) -> Unit:
84
+ """Climb to the outermost unit (typically the :class:`PassSequence`)."""
85
+ while unit.parent is not None:
86
+ unit = unit.parent
87
+ return unit
88
+
89
+
90
+ def _leaf_out_profiles(unit: Unit):
91
+ """Yield the out-profiles of all leaf units in solution order.
92
+
93
+ Cooling units are subdivided into disk elements; those disks are the leaves
94
+ and provide the fine ``(t, T)`` resolution. Units without subunits (e.g.
95
+ roll passes) contribute their own out-profile.
96
+ """
97
+ subunits = unit.subunits
98
+ if subunits:
99
+ for sub in subunits:
100
+ yield from _leaf_out_profiles(sub)
101
+ elif unit.out_profile is not None:
102
+ yield unit.out_profile
103
+
104
+
105
+ def _ring_temperatures(profile):
106
+ if profile.has_set_or_cached("ring_temperatures"):
107
+ value = profile.ring_temperatures
108
+ if value is not None:
109
+ return np.asarray(value, dtype=float)
110
+ return None
111
+
112
+
113
+ def _collect_history(target):
114
+ """Collect ``(t, temperature, ring_temperatures)`` samples up to ``target``.
115
+
116
+ Returns a list of ``(t, T, ring_T_or_None)`` tuples ordered by time,
117
+ starting from the first in-profile of the sequence and ending at ``target``.
118
+ Profiles later than ``target`` (downstream, or not yet solved) are excluded
119
+ via the ``t`` filter.
120
+ """
121
+ root = _root_unit(target.unit)
122
+ t_target = target.t
123
+
124
+ samples = []
125
+
126
+ start = root.in_profile
127
+ if start is not None and start.has_value("t") and start.has_value("temperature"):
128
+ samples.append((float(start.t), float(start.temperature), _ring_temperatures(start)))
129
+
130
+ for profile in _leaf_out_profiles(root):
131
+ if profile.t <= t_target + 1e-9:
132
+ samples.append(
133
+ (float(profile.t), float(profile.temperature), _ring_temperatures(profile))
134
+ )
135
+
136
+ samples.sort(key=lambda s: s[0])
137
+ return samples
138
+
139
+
140
+ def _scalar_history(target):
141
+ """Return ``(time, temperature)`` arrays of the bulk cycle, or ``None``."""
142
+ samples = _collect_history(target)
143
+ if len(samples) < 2:
144
+ return None
145
+ times = np.array([s[0] for s in samples], dtype=float)
146
+ temperatures = np.array([s[1] for s in samples], dtype=float)
147
+ return times, temperatures
148
+
149
+
150
+ def _ring_history(target):
151
+ """Return ``(time, ring_temperatures_2d)`` of the per-ring cycles, or ``None``."""
152
+ samples = _collect_history(target)
153
+ if len(samples) < 2 or any(s[2] is None for s in samples):
154
+ return None
155
+ if len({len(s[2]) for s in samples}) != 1:
156
+ return None
157
+ times = np.array([s[0] for s in samples], dtype=float)
158
+ ring_temperatures = np.array([s[2] for s in samples], dtype=float)
159
+ return times, ring_temperatures
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Kinetics evaluation
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def _solve_cycle(solver, profile, times, temperatures, n):
167
+ """Run the kinetics solver on a single ``T(t)`` cycle.
168
+
169
+ Resamples ``(times, temperatures)`` onto the uniform grid the solver
170
+ expects and short-circuits to a fully austenitic frame when the cycle never
171
+ cools below the ferrite-start temperature, sparing the per-step root solve
172
+ for the still-hot part of the line.
173
+ """
174
+ times = np.asarray(times, dtype=float)
175
+ temperatures = np.asarray(temperatures, dtype=float)
176
+ unique_times, order = np.unique(times, return_index=True)
177
+ grid = np.linspace(unique_times[0], unique_times[-1], n)
178
+ grid_temperatures = np.interp(grid, unique_times, temperatures[order])
179
+
180
+ if np.nanmin(grid_temperatures) >= profile.ae3_temperature:
181
+ frame = pd.DataFrame({"t": grid, "T": grid_temperatures})
182
+ for name in ("ferrite", "perlite", "bainite", "martensite"):
183
+ frame[name] = 0.0
184
+ frame["austenite"] = 1.0
185
+ return frame
186
+
187
+ return solver.get_transformed_fraction(grid, grid_temperatures)
188
+
189
+
190
+ def _final_composition(frame: pd.DataFrame) -> dict:
191
+ final = frame.iloc[-1]
192
+ return {name: float(final[name]) for name in _PHASES}
193
+
194
+
195
+ def _bulk_frame(profile):
196
+ """The solver frame for the bulk thermal history, cached on the profile."""
197
+ cached = profile.__dict__.get(_FRAME_CACHE)
198
+ if cached is not None:
199
+ return cached
200
+ history = _scalar_history(profile)
201
+ if history is None:
202
+ return None
203
+ solver = PhaseTransformationSolver(profile)
204
+ frame = _solve_cycle(solver, profile, history[0], history[1], Config.THERMAL_HISTORY_SAMPLES)
205
+ profile.__dict__[_FRAME_CACHE] = frame
206
+ return frame
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # Hooks
211
+ # ---------------------------------------------------------------------------
212
+
213
+ @Unit.OutProfile.thermal_history
214
+ def thermal_history(self):
215
+ history = _scalar_history(self)
216
+ if history is None:
217
+ return None
218
+ return {"t": history[0], "T": history[1]}
219
+
220
+
221
+ @Unit.OutProfile.ring_thermal_histories
222
+ def ring_thermal_histories(self):
223
+ history = _ring_history(self)
224
+ if history is None:
225
+ return None
226
+ return history[1]
227
+
228
+
229
+ @Unit.OutProfile.microstructure_composition
230
+ def microstructure_composition(self):
231
+ frame = _bulk_frame(self)
232
+ if frame is None:
233
+ return None
234
+ return _final_composition(frame)
235
+
236
+
237
+ @Unit.OutProfile.ring_microstructure_compositions
238
+ def ring_microstructure_compositions(self):
239
+ history = _ring_history(self)
240
+ if history is None:
241
+ return None
242
+ times, ring_temperatures = history
243
+
244
+ # The kinetics inputs (transformation temperatures, composition factors,
245
+ # grain size) are profile-level and identical for every ring; only the
246
+ # temperature path differs, so one solver instance is reused.
247
+ solver = PhaseTransformationSolver(self)
248
+ n = Config.THERMAL_HISTORY_SAMPLES
249
+
250
+ n_rings = ring_temperatures.shape[1]
251
+ result = np.empty(n_rings, dtype=object)
252
+ for i in range(n_rings):
253
+ frame = _solve_cycle(solver, self, times, ring_temperatures[:, i], n)
254
+ result[i] = _final_composition(frame)
255
+ return result
256
+
257
+
258
+ @Unit.OutProfile.vickers_hardness
259
+ def vickers_hardness(self):
260
+ frame = _bulk_frame(self)
261
+ if frame is None:
262
+ return None
263
+ hardness = PhaseTransformationSolver(self).estimate_hardness(frame)
264
+ if not np.isfinite(hardness):
265
+ return None
266
+ return float(hardness)
267
+
268
+
269
+ @Unit.OutProfile.mechanical_properties
270
+ def mechanical_properties(self):
271
+ frame = _bulk_frame(self)
272
+ if frame is None:
273
+ return None
274
+ properties = PhaseTransformationSolver(self).estimate_mechanical_properties(frame)
275
+ return {key: float(value) for key, value in properties.items()}
@@ -0,0 +1,6 @@
1
+ from .base import PhaseTransformation
2
+ from .diffusional_phase_transformation import DiffusionalPhaseTransformation
3
+ from .bainite import Bainite
4
+ from .ferrite import Ferrite
5
+ from .perlite import Perlite
6
+ from .martensite import Martensite
@@ -0,0 +1,29 @@
1
+ from .diffusional_phase_transformation import DiffusionalPhaseTransformation
2
+
3
+
4
+ class Bainite(DiffusionalPhaseTransformation):
5
+ """
6
+ Austenite to bainite phase transformation
7
+ """
8
+
9
+ name = "bainite"
10
+
11
+ @property
12
+ def grain_size_coefficient(self):
13
+ return 0.29
14
+
15
+ @property
16
+ def diffusion_coefficient(self):
17
+ return 2
18
+
19
+ @property
20
+ def transformation_start_temperature(self):
21
+ return self.profile.bainite_start_temperature
22
+
23
+ @property
24
+ def transformation_finish_temperature(self):
25
+ return self.profile.martensite_start_temperature
26
+
27
+ @property
28
+ def composition_factor(self):
29
+ return self.profile.bainite_kinetics
@@ -0,0 +1,52 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ import numpy as np
4
+ import pyroll.core as pr
5
+
6
+
7
+ class PhaseTransformation(ABC):
8
+ """
9
+ Abstract base class for an austenite-decomposition product.
10
+
11
+ A phase only has to know how to evaluate the fraction transformed along a
12
+ thermal history ``T(t)``. Diffusional products (ferrite, pearlite,
13
+ bainite) additionally provide isothermal kinetics and live in
14
+ :class:`DiffusionalPhaseTransformation`; athermal products (martensite)
15
+ implement :meth:`get_transformed_fraction` directly.
16
+
17
+ Attributes
18
+ ----------
19
+ name : str
20
+ Identifier of the phase. It is also the column key used in the result
21
+ frames produced by the solver and the colour key used by the diagram
22
+ utilities, so it must be unique across all registered phases.
23
+ """
24
+
25
+ name: str
26
+
27
+ def __init__(self, profile: pr.Profile):
28
+ self.profile: pr.Profile = profile
29
+
30
+ @abstractmethod
31
+ def get_transformed_fraction(
32
+ self, time: np.ndarray, temperature: np.ndarray, n: int = 1000
33
+ ):
34
+ """
35
+ Calculate the transformed fraction for a given thermal cycle ``T(t)``.
36
+
37
+ Parameters
38
+ ----------
39
+ time : np.ndarray
40
+ Time in seconds.
41
+ temperature : np.ndarray
42
+ Temperatures at the instants of ``time`` in Kelvin.
43
+ n : int, optional
44
+ Number of points at which the transformed fraction is evaluated.
45
+
46
+ Returns
47
+ -------
48
+ time, temperature, fraction : tuple of np.ndarray
49
+ The (possibly resampled) thermal cycle and the transformed phase
50
+ fraction evaluated along it.
51
+ """
52
+ raise NotImplementedError