pyroll-pillar-model-karman 3.1.0b1__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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ tests/output/
7
+ /.idea/misc.xml
8
+ /.idea/modules.xml
9
+ /.idea/inspectionProfiles/profiles_settings.xml
10
+ /.idea/pyroll-pillar-model-karman.iml
11
+ /.idea/vcs.xml
@@ -0,0 +1,11 @@
1
+ Copyright 2024 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,33 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyroll-pillar-model-karman
3
+ Version: 3.1.0b1
4
+ Summary: Plugin for PyRolL providing a pillar-wise calculation of von-Karmans differential-equation.
5
+ Project-URL: Homepage, https://pyroll.readthedocs.io
6
+ Project-URL: Repository, https://github.com/pyroll-project/pyroll-pillar-model-karman
7
+ Project-URL: Documentation, https://github.com/pyroll-project/pyroll-pillar-model-karman/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: plotly
30
+ Requires-Dist: pyroll-core~=3.1
31
+ Requires-Dist: pyroll-freiberg-flow-stress~=3.0
32
+ Requires-Dist: pyroll-interface-friction~=3.0
33
+ Requires-Dist: pyroll-pillar-model~=3.0
@@ -0,0 +1,40 @@
1
+ [build]
2
+ packages = [
3
+ "pyroll"
4
+ ]
5
+
6
+ [metadata]
7
+ allow-direct-references = true
8
+
9
+ [version]
10
+ path = "pyroll/pillar_model_karman/__init__.py"
11
+
12
+ [envs.default]
13
+ path = ".venv"
14
+ dependencies = [
15
+ "pytest ~= 7.0",
16
+ "matplotlib ~= 3.6",
17
+ "pyroll-report ~= 3.0",
18
+ "pyroll-sparling-spreading",
19
+ "imageio-ffmpeg ~= 0.5",
20
+ ]
21
+
22
+ [envs.docs]
23
+ path = ".docs_venv"
24
+ dependencies = [
25
+ "pygments ~= 2.0",
26
+ "pytask",
27
+ "pytask-latex",
28
+ "matplotlib"
29
+ ]
30
+ [envs.docs.scripts]
31
+ build = "python -m pytask"
32
+
33
+ [envs.test]
34
+ path = ""
35
+
36
+ [envs.test.scripts]
37
+ all = "pytest"
38
+
39
+ [[envs.test.matrix]]
40
+ python = ["3.9", "3.10", "3.11"]
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "pyroll-pillar-model-karman"
3
+ description = "Plugin for PyRolL providing a pillar-wise calculation of von-Karmans differential-equation."
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 ~= 3.1",
12
+ "pyroll-pillar-model ~= 3.0",
13
+ "pyroll-freiberg-flow-stress~=3.0",
14
+ "pyroll-interface-friction~=3.0",
15
+ "plotly",
16
+ "pandas"
17
+ ]
18
+
19
+ classifiers = [
20
+ "Intended Audience :: Science/Research",
21
+ "Intended Audience :: Manufacturing",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Scientific/Engineering",
24
+ "License :: OSI Approved :: BSD License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Development Status :: 5 - Production/Stable",
30
+ "Framework :: Hatch",
31
+ "Framework :: IPython",
32
+ "Framework :: Jupyter",
33
+ "Framework :: Pytest",
34
+ "Framework :: Sphinx",
35
+ "Natural Language :: English",
36
+ ]
37
+
38
+ dynamic = ["version"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://pyroll.readthedocs.io"
42
+
43
+
44
+ Repository = "https://github.com/pyroll-project/pyroll-pillar-model-karman"
45
+ Documentation = "https://github.com/pyroll-project/pyroll-pillar-model-karman/blob/main/docs/docs.pdf"
46
+
47
+ [build-system]
48
+ requires = ["hatchling"]
49
+ build-backend = "hatchling.build"
@@ -0,0 +1,26 @@
1
+ import importlib.util
2
+ from . import roll_pass
3
+ from pyroll.core import config as _config
4
+
5
+ VERSION = "3.1.0b1"
6
+
7
+ REPORT_INSTALLED = bool(importlib.util.find_spec("pyroll.report"))
8
+
9
+ if REPORT_INSTALLED:
10
+ from . import report
11
+ import pyroll.report
12
+
13
+ pyroll.report.plugin_manager.register(report)
14
+
15
+
16
+ @_config("PYROLL_3D_KARMAN_MODEL")
17
+ class Config:
18
+ SHEAR_STRESS_DIRECTION_REGULARISATION = 0.001
19
+ SHEAR_STRESS_PRESSURE_REGULARISATION = 0.001
20
+ LATERAL_SHEAR_STRESS_DIRECTION_REGULARISATION = 0.05
21
+ USE_SPREADING_VELOCITY_REGULARISATION = True
22
+ """If True, the latitudinal friction direction is regularized from each pillar's actual
23
+ lateral (spreading) velocity (can flip sign locally, e.g. "Querfliessen"); requires a
24
+ spread model that resolves non-trivial pillar_spreads. If False, falls back to the
25
+ purely geometric tanh(z / (eps * b/2)), which always assumes outward flow away from the
26
+ centerline."""
@@ -0,0 +1,160 @@
1
+ import numpy as np
2
+ import plotly.io as pio
3
+ import matplotlib.pyplot as plt
4
+ import plotly.graph_objects as go
5
+
6
+ from pyroll.core import Unit, RollPass
7
+ from pyroll.report import hookimpl
8
+
9
+
10
+ def _mirror_pillars(pillars: np.ndarray) -> np.ndarray:
11
+ """Mirror the half-width pillar z-coordinates (``pillars[0]`` at the center) to the full
12
+ profile width."""
13
+ return np.concatenate([-pillars[:0:-1], pillars])
14
+
15
+
16
+ def _mirror_values(values: np.ndarray, antisymmetric: bool) -> np.ndarray:
17
+ """Mirror a per-pillar quantity from half- to full-width. Stresses and friction stresses
18
+ acting in the rolling or roll-normal direction are symmetric about the profile center
19
+ (``antisymmetric=False``); the latitudinal (width-direction) friction stress reverses sign
20
+ on the opposite half of the profile, since it opposes the (mirrored) outward material flow
21
+ there, so it is mirrored antisymmetrically (``antisymmetric=True``)."""
22
+ mirrored_half = -values[:0:-1] if antisymmetric else values[:0:-1]
23
+ return np.concatenate([mirrored_half, values])
24
+
25
+
26
+ def _full_width_surface(rp: RollPass, attr: str, antisymmetric: bool = False):
27
+ x = np.array([
28
+ np.ones_like(_mirror_pillars(de.out_profile.pillars)) * de.out_profile.x
29
+ for de in rp.disk_elements
30
+ ])
31
+ y = np.array([_mirror_pillars(de.out_profile.pillars) for de in rp.disk_elements])
32
+ z = np.array([
33
+ _mirror_values(getattr(de.out_profile, attr), antisymmetric) for de in rp.disk_elements
34
+ ])
35
+ return x, y, z
36
+
37
+
38
+ @hookimpl(specname="unit_plot")
39
+ def roll_pass_altitudinal_stress_stress_profile(unit: Unit):
40
+ if isinstance(unit, RollPass) and unit.disk_elements:
41
+ rp: RollPass = unit
42
+ x, y, z = _full_width_surface(rp, "pillars_altitudinal_stress")
43
+
44
+ fig = go.Figure(data=[go.Surface(z=z, x=x, y=y)])
45
+ fig.update_layout(title='Workpiece Altitudinal Stress Profile')
46
+ fig.update_layout(
47
+ scene=dict(
48
+ xaxis=dict(title='Contact Length', gridcolor='rgb(200, 200, 200)', showgrid=True),
49
+ yaxis=dict(title='Profile Width', gridcolor='rgb(200, 200, 200)', showgrid=True),
50
+ zaxis=dict(title='Altitudinal Stress', gridcolor='rgb(200, 200, 200)', showgrid=True),
51
+ ),
52
+ title='Workpiece Altitudinal Stress Profile',
53
+ width=1200,
54
+ height=800,
55
+ template='simple_white'
56
+ )
57
+
58
+ html_string = pio.to_html(fig, full_html=False, include_plotlyjs='cdn')
59
+
60
+ return html_string
61
+
62
+
63
+ @hookimpl(specname="unit_plot")
64
+ def roll_pass_latitudinal_stress_profile(unit: Unit):
65
+ if isinstance(unit, RollPass) and unit.disk_elements:
66
+ rp: RollPass = unit
67
+ x, y, z = _full_width_surface(rp, "pillars_latitudinal_stress")
68
+
69
+ fig = go.Figure(data=[go.Surface(z=z, x=x, y=y)])
70
+ fig.update_layout(title='Workpiece Latitudinal Stress Profile')
71
+ fig.update_layout(
72
+ scene=dict(
73
+ xaxis=dict(title='Contact Length', gridcolor='rgb(200, 200, 200)', showgrid=True),
74
+ yaxis=dict(title='Profile Width', gridcolor='rgb(200, 200, 200)', showgrid=True),
75
+ zaxis=dict(title='Latitudinal Stress', gridcolor='rgb(200, 200, 200)', showgrid=True),
76
+ ),
77
+ title='Workpiece Latitudinal Stress Profile',
78
+ width=1200,
79
+ height=800,
80
+ template='simple_white'
81
+ )
82
+
83
+ html_string = pio.to_html(fig, full_html=False, include_plotlyjs='cdn')
84
+
85
+ return html_string
86
+
87
+
88
+ @hookimpl(specname="unit_plot")
89
+ def roll_pass_longitudinal_stress_profile(unit: Unit):
90
+ if isinstance(unit, RollPass) and unit.disk_elements:
91
+ rp: RollPass = unit
92
+ x, y, z = _full_width_surface(rp, "pillars_longitudinal_stress")
93
+
94
+ fig = go.Figure(data=[go.Surface(z=z, x=x, y=y)])
95
+ fig.update_layout(title='Workpiece Longitudinal Stress Profile')
96
+ fig.update_layout(
97
+ scene=dict(
98
+ xaxis=dict(title='Contact Length', gridcolor='rgb(200, 200, 200)', showgrid=True),
99
+ yaxis=dict(title='Profile Width', gridcolor='rgb(200, 200, 200)', showgrid=True),
100
+ zaxis=dict(title='Logitudinal Stress', gridcolor='rgb(200, 200, 200)', showgrid=True),
101
+ ),
102
+ title='Workpiece Longitudinal Stress Profile',
103
+ width=1200,
104
+ height=800,
105
+ template='simple_white'
106
+ )
107
+
108
+ html_string = pio.to_html(fig, full_html=False, include_plotlyjs='cdn')
109
+
110
+ return html_string
111
+
112
+
113
+ @hookimpl(specname="unit_plot")
114
+ def roll_pass_pillars_latitudinal_friction_stress_profile(unit: Unit):
115
+ if isinstance(unit, RollPass) and unit.disk_elements:
116
+ rp: RollPass = unit
117
+ x, y, z = _full_width_surface(rp, "pillars_latitudinal_friction_stress", antisymmetric=True)
118
+
119
+ fig = go.Figure(data=[go.Surface(z=z, x=x, y=y)])
120
+ fig.update_layout(title='Workpiece Latitudinal Shear Stress')
121
+ fig.update_layout(
122
+ scene=dict(
123
+ xaxis=dict(title='Contact Length', gridcolor='rgb(200, 200, 200)', showgrid=True),
124
+ yaxis=dict(title='Profile Width', gridcolor='rgb(200, 200, 200)', showgrid=True),
125
+ zaxis=dict(title='Latitudinal Shear Stress', gridcolor='rgb(200, 200, 200)', showgrid=True),
126
+ ),
127
+ title='Workpiece Latitudinal Shear Stress Profile',
128
+ width=1200,
129
+ height=800,
130
+ template='simple_white'
131
+ )
132
+
133
+ html_string = pio.to_html(fig, full_html=False, include_plotlyjs='cdn')
134
+
135
+ return html_string
136
+
137
+
138
+ @hookimpl(specname="unit_plot")
139
+ def roll_pass_pillars_longitudinal_friction_stress_profile(unit: Unit):
140
+ if isinstance(unit, RollPass) and unit.disk_elements:
141
+ rp: RollPass = unit
142
+ x, y, z = _full_width_surface(rp, "pillars_longitudinal_friction_stress")
143
+
144
+ fig = go.Figure(data=[go.Surface(z=z, x=x, y=y)])
145
+ fig.update_layout(title='Workpiece Longitudinal Shear Stress')
146
+ fig.update_layout(
147
+ scene=dict(
148
+ xaxis=dict(title='Contact Length', gridcolor='rgb(200, 200, 200)', showgrid=True),
149
+ yaxis=dict(title='Profile Width', gridcolor='rgb(200, 200, 200)', showgrid=True),
150
+ zaxis=dict(title='Longitudinal Shear Stress', gridcolor='rgb(200, 200, 200)', showgrid=True),
151
+ ),
152
+ title='Workpiece Longitudinal Shear Stress Profile',
153
+ width=1200,
154
+ height=800,
155
+ template='simple_white'
156
+ )
157
+
158
+ html_string = pio.to_html(fig, full_html=False, include_plotlyjs='cdn')
159
+
160
+ return html_string
@@ -0,0 +1,3 @@
1
+ from . import pillar_disk_element
2
+ from . import hookimpls
3
+ from . import additional_inits
@@ -0,0 +1,80 @@
1
+ import numpy as np
2
+ from pyroll.core import Profile as BaseProfile
3
+ from pyroll.core import SymmetricRollPass, Unit
4
+
5
+ from . import stress_solver as ss
6
+
7
+
8
+ class KarmanInitialSolver(Unit):
9
+ def __init__(self, disk_element: SymmetricRollPass.DiskElement):
10
+ self.de = disk_element
11
+ super().__init__(label=f"Initial Solution for {self.de.roll_pass}")
12
+
13
+ def solve(self, in_profile: BaseProfile) -> BaseProfile:
14
+ if self.de is not self.de.roll_pass.disk_elements[0]:
15
+ return in_profile
16
+
17
+ from .. import Config
18
+
19
+ roll_pass = self.de.roll_pass
20
+ mu = roll_pass.coulomb_friction_coefficient
21
+ longitudinal_direction = 1.0
22
+ # geometric regularization only: this lumped entry-wedge step has no in/out pillar
23
+ # pair to derive an actual lateral velocity from, unlike the per-disk-element solver
24
+ # (see hookimpls/pillar_disk_element.py).
25
+ lateral_scale = Config.LATERAL_SHEAR_STRESS_DIRECTION_REGULARISATION * in_profile.pillar_boundaries[-1]
26
+ latitudinal_directions = np.tanh(in_profile.pillars / lateral_scale)
27
+ longitudinal_angles = roll_pass.roll.pillar_entry_angles
28
+
29
+ n = len(in_profile.pillars)
30
+ flow_stresses = np.full(n, in_profile.flow_stress)
31
+ longitudinal_stresses = np.full(n, roll_pass.back_tension, dtype=float)
32
+ altitudinal_stresses = np.zeros(n)
33
+ latitudinal_stresses = np.zeros(n)
34
+ normal_stresses = np.zeros(n)
35
+ longitudinal_friction_stresses = np.zeros(n)
36
+ latitudinal_friction_stresses = np.zeros(n)
37
+
38
+ latitudinal_stress = 0.0
39
+ for i in range(n - 1, -1, -1):
40
+ longitudinal_angle = longitudinal_angles[i]
41
+ latitudinal_angle = in_profile.pillar_latitudinal_angles[i]
42
+ latitudinal_direction = latitudinal_directions[i]
43
+ flow_stress = flow_stresses[i]
44
+
45
+ altitudinal_stress = ss.altitudinal_stress(longitudinal_stresses[i], latitudinal_stress, flow_stress)
46
+ normal_stress = ss.normal_stress(
47
+ altitudinal_stress, longitudinal_angle, latitudinal_angle,
48
+ longitudinal_direction, latitudinal_direction, mu,
49
+ )
50
+ longitudinal_friction_stress = ss.longitudinal_friction_stress(
51
+ normal_stress, latitudinal_angle, longitudinal_direction, mu
52
+ )
53
+ latitudinal_friction_stress = ss.latitudinal_friction_stress(
54
+ normal_stress, longitudinal_angle, latitudinal_direction, mu
55
+ )
56
+ latitudinal_stress_increment = ss.latitudinal_stress_increment(
57
+ normal_stress, latitudinal_friction_stress, latitudinal_stress,
58
+ longitudinal_angle, latitudinal_angle,
59
+ in_profile.pillar_latitudinal_height_derivatives[i], in_profile.pillar_heights[i],
60
+ )
61
+
62
+ altitudinal_stresses[i] = altitudinal_stress
63
+ normal_stresses[i] = normal_stress
64
+ latitudinal_stresses[i] = latitudinal_stress
65
+ longitudinal_friction_stresses[i] = longitudinal_friction_stress
66
+ latitudinal_friction_stresses[i] = latitudinal_friction_stress
67
+
68
+ latitudinal_stress = latitudinal_stress + latitudinal_stress_increment * in_profile.pillar_widths[i]
69
+
70
+ in_profile.pillars_longitudinal_stress = longitudinal_stresses
71
+ in_profile.pillars_altitudinal_stress = altitudinal_stresses
72
+ in_profile.pillars_latitudinal_stress = latitudinal_stresses
73
+ in_profile.pillars_normal_stress = normal_stresses
74
+ in_profile.pillars_longitudinal_friction_stress = longitudinal_friction_stresses
75
+ in_profile.pillars_latitudinal_friction_stress = latitudinal_friction_stresses
76
+
77
+ return in_profile
78
+
79
+
80
+ SymmetricRollPass.DiskElement.pre_processors.append(KarmanInitialSolver)
@@ -0,0 +1 @@
1
+ from . import pillar_disk_element
@@ -0,0 +1,172 @@
1
+ import numpy as np
2
+
3
+ from ..pillar_disk_element import PillarDiskElement
4
+ from .. import stress_solver as ss
5
+
6
+
7
+ @PillarDiskElement.longitudinal_direction
8
+ def longitudinal_direction(self: PillarDiskElement):
9
+ from ... import Config
10
+ return 2 / np.pi * np.arctan(
11
+ (self.roll_pass.velocity - self.out_profile.velocity) / Config.SHEAR_STRESS_DIRECTION_REGULARISATION)
12
+
13
+
14
+ @PillarDiskElement.pillar_latitudinal_directions
15
+ def pillar_latitudinal_directions(self: PillarDiskElement):
16
+ from ... import Config
17
+ ip = self.in_profile
18
+ op = self.out_profile
19
+
20
+ if not Config.USE_SPREADING_VELOCITY_REGULARISATION:
21
+ half_width = ip.pillar_boundaries[-1]
22
+ geometric_scale = Config.LATERAL_SHEAR_STRESS_DIRECTION_REGULARISATION * half_width
23
+ return np.tanh(ip.pillars / geometric_scale)
24
+
25
+ # actual lateral (spreading) velocity of each pillar, from how far its z-position
26
+ # shifted across this disk element; unlike a purely geometric sign(z), this can flip
27
+ # sign locally (e.g. material flowing towards the center despite positive overall
28
+ # spread, "Querfliessen", see Schlegel 1976) whenever the underlying spread model
29
+ # resolves it. Requires a real spread model to be loaded (pillar_spreads != 1
30
+ # everywhere) so the lateral velocity is actually non-zero.
31
+ lateral_velocity = (op.pillars - ip.pillars) / self.length * self.pillar_velocities
32
+ # tiny floor: at the very first disk element no pillar has started spreading yet, so
33
+ # both the velocity and its max are genuinely 0 there (not a missing spread model).
34
+ scale = Config.LATERAL_SHEAR_STRESS_DIRECTION_REGULARISATION * np.max(np.abs(lateral_velocity)) + 1e-30
35
+ return np.tanh(lateral_velocity / scale)
36
+
37
+
38
+ class KarmanSolution3D:
39
+ """Solves the pillar-wise 3D von Karman equations for one disk element: first the longitudinal
40
+ (rolling direction) stress increment for all pillars at once, then a sweep over the pillars from
41
+ the free edge (latitudinal stress = 0 boundary condition) towards the center, integrating the
42
+ latitudinal stress increment inward. See Overhagen (2018), section 4.5.5, for the discretization
43
+ scheme this mirrors."""
44
+
45
+ def __init__(self, disk_element: PillarDiskElement):
46
+ self.disk_element = disk_element
47
+ self.roll_pass = disk_element.roll_pass
48
+ self.in_profile = disk_element.in_profile
49
+ self.out_profile = disk_element.out_profile
50
+ self.number_of_pillars = len(self.in_profile.pillars)
51
+ self.solution = self.solve()
52
+
53
+ def calculate_longitudinal_stresses(self):
54
+ ip = self.in_profile
55
+ de = self.disk_element
56
+ mu = self.roll_pass.coulomb_friction_coefficient
57
+
58
+ longitudinal_angle = de.pillar_longitudinal_angles
59
+ latitudinal_angle = ip.pillar_latitudinal_angles
60
+
61
+ friction_x = ss.longitudinal_friction_stress(
62
+ ip.pillars_normal_stress, latitudinal_angle, de.longitudinal_direction, mu
63
+ )
64
+ increment = ss.longitudinal_stress_increment(
65
+ ip.pillars_normal_stress, friction_x, ip.pillars_longitudinal_stress,
66
+ longitudinal_angle, latitudinal_angle,
67
+ de.pillar_longitudinal_height_derivatives, ip.pillar_heights,
68
+ )
69
+
70
+ return ip.pillars_longitudinal_stress + increment * de.length
71
+
72
+ def calculate_stresses(self, index: int, latitudinal_stress: float, longitudinal_stress: float):
73
+ op = self.out_profile
74
+ de = self.disk_element
75
+ mu = self.roll_pass.coulomb_friction_coefficient
76
+
77
+ longitudinal_angle = de.pillar_longitudinal_angles[index]
78
+ latitudinal_angle = op.pillar_latitudinal_angles[index]
79
+ latitudinal_direction = de.pillar_latitudinal_directions[index]
80
+ flow_stress = op.pillars_flow_stress[index]
81
+
82
+ altitudinal_stress = ss.altitudinal_stress(longitudinal_stress, latitudinal_stress, flow_stress)
83
+ normal_stress = ss.normal_stress(
84
+ altitudinal_stress, longitudinal_angle, latitudinal_angle,
85
+ de.longitudinal_direction, latitudinal_direction, mu,
86
+ )
87
+ longitudinal_friction_stress = ss.longitudinal_friction_stress(
88
+ normal_stress, latitudinal_angle, de.longitudinal_direction, mu
89
+ )
90
+ latitudinal_friction_stress = ss.latitudinal_friction_stress(
91
+ normal_stress, longitudinal_angle, latitudinal_direction, mu
92
+ )
93
+ latitudinal_stress_increment = ss.latitudinal_stress_increment(
94
+ normal_stress, latitudinal_friction_stress, latitudinal_stress,
95
+ longitudinal_angle, latitudinal_angle,
96
+ op.pillar_latitudinal_height_derivatives[index], op.pillar_heights[index],
97
+ )
98
+
99
+ return {
100
+ "altitudinal_stress": altitudinal_stress,
101
+ "normal_stress": normal_stress,
102
+ "longitudinal_friction_stress": longitudinal_friction_stress,
103
+ "latitudinal_friction_stress": latitudinal_friction_stress,
104
+ "latitudinal_stress_increment": latitudinal_stress_increment,
105
+ }
106
+
107
+ def solve(self):
108
+ n = self.number_of_pillars
109
+ longitudinal_stresses = self.calculate_longitudinal_stresses()
110
+
111
+ altitudinal_stresses = np.zeros(n)
112
+ latitudinal_stresses = np.zeros(n)
113
+ longitudinal_friction_stresses = np.zeros(n)
114
+ latitudinal_friction_stresses = np.zeros(n)
115
+ normal_stresses = np.zeros(n)
116
+
117
+ latitudinal_stress = 0.0
118
+ for i in range(n - 1, -1, -1):
119
+ result = self.calculate_stresses(i, latitudinal_stress, longitudinal_stresses[i])
120
+
121
+ altitudinal_stresses[i] = result["altitudinal_stress"]
122
+ latitudinal_stresses[i] = latitudinal_stress
123
+ longitudinal_friction_stresses[i] = result["longitudinal_friction_stress"]
124
+ latitudinal_friction_stresses[i] = result["latitudinal_friction_stress"]
125
+ normal_stresses[i] = result["normal_stress"]
126
+
127
+ latitudinal_stress = latitudinal_stress + result["latitudinal_stress_increment"] * \
128
+ self.out_profile.pillar_widths[i]
129
+
130
+ return {
131
+ "longitudinal_stresses": longitudinal_stresses,
132
+ "altitudinal_stresses": altitudinal_stresses,
133
+ "latitudinal_stresses": latitudinal_stresses,
134
+ "longitudinal_friction_stresses": longitudinal_friction_stresses,
135
+ "latitudinal_friction_stresses": latitudinal_friction_stresses,
136
+ "normal_stresses": normal_stresses,
137
+ }
138
+
139
+
140
+ @PillarDiskElement.karman_solution
141
+ def karman_solution(self: PillarDiskElement):
142
+ return KarmanSolution3D(disk_element=self).solution
143
+
144
+
145
+ @PillarDiskElement.OutProfile.pillars_longitudinal_stress
146
+ def pillars_longitudinal_stress(self: PillarDiskElement.OutProfile):
147
+ return self.disk_element.karman_solution["longitudinal_stresses"]
148
+
149
+
150
+ @PillarDiskElement.OutProfile.pillars_altitudinal_stress
151
+ def pillars_altitudinal_stress(self: PillarDiskElement.OutProfile):
152
+ return self.disk_element.karman_solution["altitudinal_stresses"]
153
+
154
+
155
+ @PillarDiskElement.OutProfile.pillars_latitudinal_stress
156
+ def pillars_latitudinal_stress(self: PillarDiskElement.OutProfile):
157
+ return self.disk_element.karman_solution["latitudinal_stresses"]
158
+
159
+
160
+ @PillarDiskElement.OutProfile.pillars_normal_stress
161
+ def pillars_normal_stress(self: PillarDiskElement.OutProfile):
162
+ return self.disk_element.karman_solution["normal_stresses"]
163
+
164
+
165
+ @PillarDiskElement.OutProfile.pillars_longitudinal_friction_stress
166
+ def pillars_longitudinal_friction_stress(self: PillarDiskElement.OutProfile):
167
+ return self.disk_element.karman_solution["longitudinal_friction_stresses"]
168
+
169
+
170
+ @PillarDiskElement.OutProfile.pillars_latitudinal_friction_stress
171
+ def pillars_latitudinal_friction_stress(self: PillarDiskElement.OutProfile):
172
+ return self.disk_element.karman_solution["latitudinal_friction_stresses"]
@@ -0,0 +1,24 @@
1
+ import pyroll
2
+ import numpy as np
3
+
4
+ from pyroll.core import RollPass, Hook
5
+
6
+
7
+ @RollPass.DiskElement.extension_class
8
+ class PillarDiskElement(RollPass.DiskElement):
9
+ karman_solution = Hook[dict]()
10
+ """Solution of the 3D von Karman's differential equation for this disk element's pillars."""
11
+
12
+ longitudinal_direction = Hook[float]()
13
+ """Regularized sign of the relative velocity between workpiece and roll surface in rolling direction."""
14
+
15
+ pillar_latitudinal_directions = Hook[np.ndarray]()
16
+ """Sign of the lateral material flow direction for each pillar (positive towards the free edge)."""
17
+
18
+
19
+ pyroll.core.root_hooks.add(pyroll.core.RollPass.DiskElement.OutProfile.pillars_normal_stress)
20
+ pyroll.core.root_hooks.add(pyroll.core.RollPass.DiskElement.OutProfile.pillars_longitudinal_stress)
21
+ pyroll.core.root_hooks.add(pyroll.core.RollPass.DiskElement.OutProfile.pillars_altitudinal_stress)
22
+ pyroll.core.root_hooks.add(pyroll.core.RollPass.DiskElement.OutProfile.pillars_latitudinal_stress)
23
+ pyroll.core.root_hooks.add(pyroll.core.RollPass.DiskElement.OutProfile.pillars_longitudinal_friction_stress)
24
+ pyroll.core.root_hooks.add(pyroll.core.RollPass.DiskElement.OutProfile.pillars_latitudinal_friction_stress)
@@ -0,0 +1,58 @@
1
+ import numpy as np
2
+
3
+
4
+ def altitudinal_stress(longitudinal_stress, latitudinal_stress, flow_stress):
5
+ """Vertical (altitudinal) stress solved from the von Mises yield criterion, neglecting the
6
+ friction shear stresses therein."""
7
+ discriminant = 4 * flow_stress ** 2 + 3 * (
8
+ 2 * longitudinal_stress * latitudinal_stress - longitudinal_stress ** 2 - latitudinal_stress ** 2
9
+ )
10
+ return (longitudinal_stress + latitudinal_stress) / 2 + 0.5 * np.sqrt(np.maximum(discriminant, 0.0))
11
+
12
+
13
+ def normal_stress(
14
+ altitudinal_stress_, longitudinal_angle, latitudinal_angle,
15
+ longitudinal_direction, latitudinal_direction, friction_coefficient,
16
+ ):
17
+ """Contact-normal stress from the altitudinal stress."""
18
+ return altitudinal_stress_ / (
19
+ 1
20
+ + longitudinal_direction * friction_coefficient * np.tan(longitudinal_angle) / np.cos(latitudinal_angle)
21
+ + latitudinal_direction * friction_coefficient * np.tan(latitudinal_angle) / np.cos(longitudinal_angle)
22
+ )
23
+
24
+
25
+ def longitudinal_friction_stress(normal_stress_, latitudinal_angle, longitudinal_direction, friction_coefficient):
26
+ """Coulomb friction stress component acting in rolling direction."""
27
+ return friction_coefficient * normal_stress_ * np.cos(latitudinal_angle) * longitudinal_direction
28
+
29
+
30
+ def latitudinal_friction_stress(normal_stress_, longitudinal_angle, latitudinal_direction, friction_coefficient):
31
+ """Coulomb friction stress component acting in width direction."""
32
+ return friction_coefficient * normal_stress_ * np.cos(longitudinal_angle) * latitudinal_direction
33
+
34
+
35
+ def longitudinal_stress_increment(
36
+ normal_stress_, longitudinal_friction_stress_, longitudinal_stress,
37
+ longitudinal_angle, latitudinal_angle, longitudinal_height_derivative, height,
38
+ ):
39
+ """d(sigma_x)/dx, expanded via the product rule d(sigma*h)/dx = h * d(sigma)/dx + sigma * dh/dx and solved for d(sigma)/dx."""
40
+ return (
41
+ 2 * normal_stress_ * np.sin(longitudinal_angle)
42
+ / (np.cos(longitudinal_angle) * np.cos(latitudinal_angle))
43
+ + 2 * longitudinal_friction_stress_ / np.cos(latitudinal_angle)
44
+ - longitudinal_stress * longitudinal_height_derivative
45
+ ) / height
46
+
47
+
48
+ def latitudinal_stress_increment(
49
+ normal_stress_, latitudinal_friction_stress_, latitudinal_stress,
50
+ longitudinal_angle, latitudinal_angle, latitudinal_height_derivative, height,
51
+ ):
52
+ """d(sigma_y)/dy expanded via the product rule, analogous to :func:`longitudinal_stress_increment`."""
53
+ return (
54
+ 2 * normal_stress_ * np.sin(latitudinal_angle)
55
+ / (np.cos(latitudinal_angle) * np.cos(longitudinal_angle))
56
+ + 2 * latitudinal_friction_stress_ / np.cos(longitudinal_angle)
57
+ - latitudinal_stress * latitudinal_height_derivative
58
+ ) / height