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.
- pyroll_phase_transformation_model-3.1.0/.gitignore +152 -0
- pyroll_phase_transformation_model-3.1.0/LICENSE +11 -0
- pyroll_phase_transformation_model-3.1.0/PKG-INFO +32 -0
- pyroll_phase_transformation_model-3.1.0/hatch.toml +32 -0
- pyroll_phase_transformation_model-3.1.0/pyproject.toml +48 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/__init__.py +43 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/microstructure.py +275 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/__init__.py +6 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/bainite.py +29 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/base.py +52 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/diffusional_phase_transformation.py +248 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/ferrite.py +29 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/martensite.py +51 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/phase_transformations/perlite.py +29 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/profile.py +165 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/__init__.py +3 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/sigmoidal_function_base.py +89 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/sigmoidal_function_i.py +18 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/sigmoidal_functions/sigmoidal_function_s.py +18 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/__init__.py +8 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/hardness.py +44 -0
- pyroll_phase_transformation_model-3.1.0/pyroll/phase_transformation_model/transformation/mechanical_properties.py +65 -0
- 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,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
|