diff --git a/pyroll/core/__init__.py b/pyroll/core/__init__.py index 5384dda..72163a1 100644 --- a/pyroll/core/__init__.py +++ b/pyroll/core/__init__.py @@ -26,7 +26,7 @@ EquivalentRibbedGroove, create_groove_by_type_name, ) -from .transport import Transport, CoolingPipe +from .transport import Transport, CoolingPipe, Spooler from .roll_pass import BaseRollPass, DeformationUnit, ThreeRollPass, SymmetricRollPass, TwoRollPass from .roll_pass import TwoRollPass as RollPass from .unit import Unit @@ -84,6 +84,7 @@ # transport "Transport", "CoolingPipe", + "Spooler", # roll_pass "RollPass", "BaseRollPass", diff --git a/pyroll/core/transport/__init__.py b/pyroll/core/transport/__init__.py index 42a634d..d2d981a 100644 --- a/pyroll/core/transport/__init__.py +++ b/pyroll/core/transport/__init__.py @@ -1,6 +1,7 @@ from .transport import Transport from .cooling_pipe import CoolingPipe +from .spooler import Spooler from . import hookimpls # noqa: F401 -__all__ = ["Transport", "CoolingPipe"] +__all__ = ["Transport", "CoolingPipe", "Spooler"] diff --git a/pyroll/core/transport/hookimpls/__init__.py b/pyroll/core/transport/hookimpls/__init__.py index e61dbc0..b3629f6 100644 --- a/pyroll/core/transport/hookimpls/__init__.py +++ b/pyroll/core/transport/hookimpls/__init__.py @@ -1,2 +1,3 @@ from . import transport from . import cooling_pipe +from . import spooler diff --git a/pyroll/core/transport/hookimpls/spooler.py b/pyroll/core/transport/hookimpls/spooler.py new file mode 100644 index 0000000..007d9ca --- /dev/null +++ b/pyroll/core/transport/hookimpls/spooler.py @@ -0,0 +1,101 @@ +import numpy as np + +from ..spooler import Spooler + + +@Spooler.duration +def duration(self: Spooler): + if self.has_set_or_cached("finished_coil_weight"): + length = self.finished_coil_weight / self.in_profile.density + return length / self.velocity + else: + return self.in_profile.length / self.velocity + + +@Spooler.windings_per_layer +def windings_per_layer(self: Spooler): + equivalent_diameter = self.in_profile.equivalent_radius * 2 + return np.round(self.mandrel_width / equivalent_diameter) + + +@Spooler.finished_coil_radius +def finished_coil_radius(self: Spooler): + return self.coil_layer_radii[-1] + + +@Spooler.coil_layer_bending_stresses +def coil_layer_bending_stresses(self: Spooler): + equivalent_diameter = self.in_profile.equivalent_radius * 2 + bending_stresses = [] + + for layer_radius in self.coil_layer_radii: + stress = self.in_profile.elastic_modulus * equivalent_diameter / (2 * layer_radius) + bending_stresses.append(stress) + + return np.array(bending_stresses) + + +@Spooler.coil_layer_bending_torques +def coil_layer_bending_torques(self: Spooler): + equivalent_diameter = self.in_profile.equivalent_radius * 2 + + section_modulus = np.pi * equivalent_diameter**3 / 32 + + bending_torques = [] + + for bending_stress in self.coil_layer_bending_stresses: + torque = bending_stress * section_modulus + bending_torques.append(torque) + + return np.array(bending_torques) + + +@Spooler.coil_layer_torque +def coil_layer_torque(self: Spooler): + torques_single_bending = self.coil_layer_bending_torques + return torques_single_bending * self.windings_per_layer + + +@Spooler.coil_cumulative_torque +def coil_cumulative_torque(self: Spooler): + return np.cumsum(self.coil_layer_torque) + + +@Spooler.coil_layer_radii +def coil_layer_radii(self: Spooler): + layer_radii = [] + layer_number = 1 + cumulative_weight = 0 + cumulative_length = 0 + equivalent_diameter = self.in_profile.equivalent_radius * 2 + + while cumulative_weight < self.finished_coil_weight and cumulative_length < self.in_profile.length: + outer_radius_current_layer = self.mandrel_radius + layer_number * equivalent_diameter + layer_radii.append(outer_radius_current_layer) + + mean_radius_current_layer = self.mandrel_radius + (layer_number - 0.5) * equivalent_diameter + wire_length_current_layer = self.windings_per_layer * 2 * np.pi * mean_radius_current_layer + + wire_volume_current_layer = wire_length_current_layer * self.in_profile.cross_section.area + weight_current_layer = wire_volume_current_layer * self.in_profile.density + + weight_ok = cumulative_weight + weight_current_layer <= self.finished_coil_weight + length_ok = cumulative_length + wire_length_current_layer <= self.in_profile.length + + if weight_ok and length_ok: + cumulative_weight += weight_current_layer + cumulative_length += wire_length_current_layer + layer_number += 1 + else: + if not weight_ok: + remaining_weight = self.finished_coil_weight - cumulative_weight + partial_layer_fraction = remaining_weight / weight_current_layer + else: + remaining_length = self.in_profile.length - cumulative_length + partial_layer_fraction = remaining_length / wire_length_current_layer + + final_radius = self.mandrel_radius + (layer_number - 1 + partial_layer_fraction) * equivalent_diameter + layer_radii[-1] = final_radius + break + + return np.array(layer_radii) diff --git a/pyroll/core/transport/spooler.py b/pyroll/core/transport/spooler.py new file mode 100644 index 0000000..bc3a9de --- /dev/null +++ b/pyroll/core/transport/spooler.py @@ -0,0 +1,96 @@ +import numpy as np +from typing import List, cast + +from ..hooks import Hook +from .transport import Transport + +__all__ = ["Spooler"] + + +class Spooler(Transport): + """Represents a spooler for spooled bar in coil.""" + + mandrel_radius = Hook[float]() + """Radius of the mandrel.""" + + mandrel_width = Hook[float]() + """Width of the mandrel.""" + + overspeed = Hook[float]() + """ Percentage of overspeed of the spooler mandrel for hooking.""" + + maximum_bending_stress = Hook[float]() + """Maximum stress to bend a wire.""" + + maximum_bending_torque = Hook[float]() + """Maximum torque to bend a wire.""" + + windings_per_layer = Hook[float]() + """Number of windings per layer.""" + + coil_layer_bending_stresses = Hook[np.ndarray]() + """Bending stress of each layer as a array.""" + + coil_layer_bending_torques = Hook[np.ndarray]() + """Bending torques of each layer as a array.""" + + coil_layer_radii = Hook[np.ndarray]() + """Radii of each layer as a array.""" + + coil_layer_torque = Hook[float]() + """Torque per layer""" + + coil_cumulative_torque = Hook[float]() + """Total required Torque for all layers""" + + finished_coil_weight = Hook[float]() + """Finished weight of the resulting coil.""" + + finished_coil_radius = Hook[float]() + """Finished radius of the resulting coil.""" + + @property + def disk_elements(self) -> List["Spooler.DiskElement"]: + """A list of disk elements used to subdivide this unit.""" + return list(self._subunits) + + class Profile(Transport.Profile): + """Represents a profile in context of a transport unit.""" + + @property + def spooler(self) -> "Spooler": + """Reference to the spooler. Alias for ``self.unit``.""" + return cast(Spooler, self.unit) + + class InProfile(Profile, Transport.InProfile): + """Represents an incoming profile of a spooler unit.""" + + class OutProfile(Profile, Transport.OutProfile): + """Represents an outgoing profile of a transport unit.""" + + class DiskElement(Transport.DiskElement): + """Represents a disk element in a roll pass.""" + + @property + def spooler(self) -> "Spooler": + """Reference to the transport. Alias for ``self.parent``.""" + return cast(Spooler, self.parent) + + class Profile(Transport.DiskElement.Profile): + """Represents a profile in context of a disk element unit.""" + + @property + def disk_element(self) -> "Spooler.DiskElement": + """Reference to the disk element. Alias for ``self.unit``""" + return cast(Spooler.DiskElement, self.unit) + + @property + def spooler(self) -> "Spooler": + """Reference to the transport. Alias for ``self.unit.parent``""" + return cast(Spooler, self.unit.parent) + + class InProfile(Profile, Transport.DiskElement.InProfile): + """Represents an incoming profile of a disk element unit.""" + + class OutProfile(Profile, Transport.DiskElement.OutProfile): + """Represents an outgoing profile of a disk element unit.""" diff --git a/tests/roll_pass/test_roll_pass_profile_location.py b/tests/roll_pass/test_roll_pass_profile_location.py index 447b152..0cf58dd 100644 --- a/tests/roll_pass/test_roll_pass_profile_location.py +++ b/tests/roll_pass/test_roll_pass_profile_location.py @@ -6,7 +6,6 @@ from pyroll.core import Profile, Roll, RollPass, CircularOvalGroove -# noinspection DuplicatedCode def test_cartesian_positions(tmp_path: Path, caplog): caplog.set_level(logging.DEBUG, logger="pyroll") diff --git a/tests/spooler/test_spooler_values.py b/tests/spooler/test_spooler_values.py new file mode 100644 index 0000000..afbb5f2 --- /dev/null +++ b/tests/spooler/test_spooler_values.py @@ -0,0 +1,102 @@ +import numpy as np +from matplotlib import pyplot as plt + +from pyroll.core import Spooler, Profile + +in_profile = Profile.round( + diameter=20e-3, flow_stress=1, velocity=100, length=5100, density=7500, elastic_modulus=8.3e10 +) + + +def test_spooler_coil_geometry(): + in_profile_geometry = Profile.round( + diameter=10e-3, flow_stress=1, velocity=100, length=5200, density=7500, elastic_modulus=8.3e10 + ) + + sp = Spooler( + label="Demo Spooler", + mandrel_radius=300e-3, + mandrel_width=800e-3, + finished_coil_weight=3000, + velocity=35, + ) + + sp.solve(in_profile_geometry) + + assert np.isclose(sp.windings_per_layer, 80) + assert np.isclose(sp.finished_coil_radius, 0.5411044, rtol=1e-5) + + +def test_spooler_bending_stress_visual(): + sp = Spooler( + label="Demo Spooler", + mandrel_radius=300e-3, + mandrel_width=800e-3, + finished_coil_weight=3200, + velocity=35, + ) + + sp.solve(in_profile) + + fig, ax = plt.subplots(figsize=(10, 6)) + + layer_numbers = range(1, len(sp.coil_layer_bending_stresses) + 1) + ax.plot(layer_numbers, sp.coil_layer_bending_stresses) + + ax.set_xlabel("Layer Number") + ax.set_ylabel("Bending Stress") + ax.set_title("Bending Stress per Layer") + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + + +def test_spooler_bending_torque_visual(): + sp = Spooler( + label="Demo Spooler", + mandrel_radius=300e-3, + mandrel_width=800e-3, + finished_coil_weight=3200, + velocity=35, + ) + + sp.solve(in_profile) + + fig, ax = plt.subplots(figsize=(10, 6)) + + layer_numbers = range(1, len(sp.coil_layer_bending_torques) + 1) + ax.plot(layer_numbers, sp.coil_layer_bending_torques) + + ax.set_xlabel("Layer Number") + ax.set_ylabel("Bending Torque") + ax.set_title("Bending Torque per Layer") + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + + +def test_spooler_cumulative_torque_visual(): + sp = Spooler( + label="Demo Spooler", + mandrel_radius=300e-3, + mandrel_width=800e-3, + finished_coil_weight=3200, + velocity=35, + ) + + sp.solve(in_profile) + + fig, ax = plt.subplots(figsize=(10, 6)) + + layer_numbers = range(1, len(sp.coil_cumulative_torque) + 1) + ax.plot(layer_numbers, sp.coil_cumulative_torque) + + ax.set_xlabel("Layer Number") + ax.set_ylabel("Torque") + ax.set_title("Cumulative Bending Torque per Layer") + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show()