diff --git a/backend/app/api/endpoints.py b/backend/app/api/endpoints.py index d137c511..5aea37fb 100644 --- a/backend/app/api/endpoints.py +++ b/backend/app/api/endpoints.py @@ -8,7 +8,7 @@ CarSpecs, solve_all, ) -from cvt_simulator.models.ramps.ramp_preview import generate_ramp_preview +from cvt_simulator.ramps.ramp_preview import generate_ramp_preview from ..models.response_models import ( FormattedResultModel, SimulationArgsInput, diff --git a/backend/app/models/auto_model.py b/backend/app/models/auto_model.py index 0f7024cb..8d9f35f7 100644 --- a/backend/app/models/auto_model.py +++ b/backend/app/models/auto_model.py @@ -11,6 +11,7 @@ get_origin, get_type_hints, ) +import enum from pydantic import BaseModel, ConfigDict, create_model _CACHE: dict[type, type[BaseModel]] = {} @@ -28,6 +29,12 @@ def _resolve(t: Any) -> Any: k_t, v_t = get_args(t) or (Any, Any) return dict[_resolve(k_t), _resolve(v_t)] # type: ignore[index] if inspect.isclass(t) and t not in _PRIMS: + # Treat Enum subclasses as simple strings to avoid deep recursive modeling + try: + if issubclass(t, enum.Enum): + return str + except Exception: + pass return model_from_class(t) return t diff --git a/cvtModel/src/cvt_simulator/__init__.py b/cvtModel/src/cvt_simulator/__init__.py index 1ec44aea..54219e6c 100644 --- a/cvtModel/src/cvt_simulator/__init__.py +++ b/cvtModel/src/cvt_simulator/__init__.py @@ -1,26 +1,39 @@ from .main import simulate_cvt_model -from .utils.simulation_args import SimulationArgs -from .models.dataTypes import ( +from .sim_utils.simulation_args import SimulationArgs +from .core.data_types import ( CvtDynamicsBreakdown, - SecondaryPulleyDynamicsBreakdown, - PrimaryPulleyDynamicsBreakdown, - SlipBreakdown, - DrivetrainBreakdown, + SecondaryForceBreakdown, + PrimaryForceBreakdown, + ContactTorqueResult, + DrivetrainAccelerationBreakdown, + ContactDynamicsBreakdown, ) -from .utils.frontend_output import FormattedSimulationResult -from .models.ramps.ramp_config import PiecewiseRampConfig -from .models.ramps.piecewise_ramp import PiecewiseRamp +from .utils.frontend_output import SimulationAnalysisResult, FormattedSimulationResult +from .ramps.ramp_config import PiecewiseRampConfig +from .ramps.piecewise_ramp import PiecewiseRamp from .constants.car_specs import CarSpecs from .solvers.solve import solve_all, AllSolverResults +# Backward-compatible aliases for older public names. +SecondaryPulleyDynamicsBreakdown = SecondaryForceBreakdown +PrimaryPulleyDynamicsBreakdown = PrimaryForceBreakdown +SlipBreakdown = ContactTorqueResult +DrivetrainBreakdown = DrivetrainAccelerationBreakdown + __all__ = [ "simulate_cvt_model", "SimulationArgs", "CvtDynamicsBreakdown", + "ContactDynamicsBreakdown", + "PrimaryForceBreakdown", + "SecondaryForceBreakdown", "SecondaryPulleyDynamicsBreakdown", "PrimaryPulleyDynamicsBreakdown", "SlipBreakdown", + "ContactTorqueResult", + "DrivetrainAccelerationBreakdown", "DrivetrainBreakdown", + "SimulationAnalysisResult", "FormattedSimulationResult", "PiecewiseRampConfig", "PiecewiseRamp", diff --git a/cvtModel/src/cvt_simulator/constants/car_specs.py b/cvtModel/src/cvt_simulator/constants/car_specs.py index 746c0a88..3fcf48f5 100644 --- a/cvtModel/src/cvt_simulator/constants/car_specs.py +++ b/cvtModel/src/cvt_simulator/constants/car_specs.py @@ -130,7 +130,7 @@ def center_to_center(self) -> float: @property def min_effective_cvt_ratio(self) -> float: """Minimum effective CVT ratio (unitless) at zero shift distance.""" - from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm + from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm return tm.current_effective_cvt_ratio(0) @@ -138,7 +138,7 @@ def min_effective_cvt_ratio(self) -> float: @property def max_effective_cvt_ratio(self) -> float: """Maximum effective CVT ratio (unitless) at max shift distance.""" - from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm + from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm return tm.current_effective_cvt_ratio(self.max_shift) diff --git a/cvtModel/src/cvt_simulator/core/components/belt_wrap.py b/cvtModel/src/cvt_simulator/core/components/belt_wrap.py new file mode 100644 index 00000000..090cf179 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/components/belt_wrap.py @@ -0,0 +1,54 @@ +"""Belt centrifugal force model. + +Calculates the radial centrifugal force component from the rotating CVT belt. +""" +import numpy as np +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.data_types import BeltWrapBreakdown +from cvt_simulator.constants.car_specs import ( + SHEAVE_ANGLE, + BELT_CROSS_SECTIONAL_AREA, +) +from cvt_simulator.constants.constants import RUBBER_DENSITY + + +class BeltWrap: + """CVT belt centrifugal force calculator.""" + + def __init__(self, is_primary: bool): + """Initialize belt with geometric constants. + + Args: + is_primary: If True, use primary pulley geometry; otherwise secondary. + """ + # Default to primary geometry + self.is_primary = is_primary + + def axial_centrifugal_force(self, state: SystemState) -> BeltWrapBreakdown: + """ + Calculate belt centrifugal force. + + Args: + state: Current system state + + Returns: + Centrifugal force [N] + """ + s = state.s + v_b = state.v_b + + # use primary/secondary wrap depending on configuration + if self.is_primary: + wrap_angle = CVT_GEOMETRY.primary_wrap_angle(s) + else: + wrap_angle = CVT_GEOMETRY.secondary_wrap_angle(s) + + beta = SHEAVE_ANGLE / 2 + + belt_force = ( + RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * v_b**2 * wrap_angle + ) / (2 * np.tan(beta)) + + return BeltWrapBreakdown(wrap_angle=wrap_angle, axial_belt_force=belt_force) + diff --git a/cvtModel/src/cvt_simulator/core/components/engine.py b/cvtModel/src/cvt_simulator/core/components/engine.py new file mode 100644 index 00000000..0302ce0a --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/components/engine.py @@ -0,0 +1,30 @@ +from typing import Callable +from cvt_simulator.core.data_types import EngineTorqueBreakdown + + +class EngineModel: + """Pure engine model handling torque curve and power calculations.""" + + def __init__( + self, + torque_curve: Callable[[float], float], # rad/s -> Nm + ): + self.torque_curve = torque_curve + + def get_torque(self, ω: float) -> float: + """Get the torque output at a given angular velocity.""" + return self.torque_curve(ω) + + def get_breakdown(self, ω: float) -> EngineTorqueBreakdown: + """Return a minimal engine breakdown (torque + power).""" + torque = self.get_torque(ω) + power = self.get_power(ω) + return EngineTorqueBreakdown( + engine_torque=torque, + engine_speed=ω, + engine_power=power + ) + + def get_power(self, ω: float) -> float: + """Get the power output at a given angular velocity.""" + return self.get_torque(ω) * ω diff --git a/cvtModel/src/cvt_simulator/core/components/primary_pulley.py b/cvtModel/src/cvt_simulator/core/components/primary_pulley.py new file mode 100644 index 00000000..ce473ca3 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/components/primary_pulley.py @@ -0,0 +1,91 @@ +"""Primary pulley clamping helper (focused, no abstracts). + +Exposes `PrimaryPulley.calculate_axial_clamping_force(shift, ω)` which returns +`(axial_force, PrimaryForceBreakdown)` and uses the existing datatypes. +""" +from cvt_simulator.ramps.piecewise_ramp import PiecewiseRamp +import numpy as np +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY +from cvt_simulator.constants.car_specs import MAX_SHIFT, INITIAL_FLYWEIGHT_RADIUS +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.data_types import ( + flyweightForceBreakdown, + springCompForceBreakdown, + PrimaryForceBreakdown, + PulleyForces, +) +from cvt_simulator.core.components.belt_wrap import BeltWrap + + +class PrimaryPulley: + """Compute primary pulley clamping (flyweight + spring). + + Parameters mirror the original component: spring coefficient, initial + compression, flyweight mass, and a `ramp` providing `height()` and `slope()`. + """ + + def __init__( + self, + spring_coeff_comp: float, + initial_compression: float, + flyweight_mass: float, + ramp: PiecewiseRamp, + initial_flyweight_radius: float = INITIAL_FLYWEIGHT_RADIUS, + ) -> None: + self.spring_coeff_comp = spring_coeff_comp + self.initial_compression = initial_compression + self.flyweight_mass = flyweight_mass + self.ramp = ramp + self.initial_flyweight_radius = initial_flyweight_radius + self.cvt = CVT_GEOMETRY + # Initialize belt wrap helper once per pulley instance + from cvt_simulator.core.components.belt_wrap import BeltWrap + self.belt_wrap = BeltWrap(is_primary=True) + + def calculate_axial_clamping_force(self, state: SystemState) -> PulleyForces: + s = float(np.clip(state.s, 0.0, MAX_SHIFT)) + + ω_p = state.ω_p + + fly = self._calculate_flyweight_force(s, ω_p) + spring = self._calculate_spring_comp_force(s) + + # belt wrap contribution (use initialized belt_wrap) + belt = self.belt_wrap.axial_centrifugal_force(state) + + axial_pulley = fly.net - spring.net + axial_total = axial_pulley + belt.axial_belt_force + + pulley_breakdown = PrimaryForceBreakdown( + flyweightForce=fly, springForce=spring, net=axial_pulley + ) + + return PulleyForces(pulley_breakdown=pulley_breakdown, belt_wrap=belt, net=axial_total) + + def _calculate_flyweight_force(self, s: float, ω: float) -> flyweightForceBreakdown: + """Compute flyweight centrifugal conversion using ramp slope. + + F_c = m * ω^2 * r_f + axial contribution = F_c * dr_f/ds + """ + flyweight_radius = self.initial_flyweight_radius + self.ramp.height(s) + centrifugal_force = self.flyweight_mass * ω**2 * flyweight_radius + ramp_gradient = self.ramp.slope(s) + net = centrifugal_force * ramp_gradient + angle = float(np.arctan(ramp_gradient)) + + return flyweightForceBreakdown( + radius=flyweight_radius, + angular_velocity=ω, + angle=angle, + centrifugal_force=centrifugal_force, + angle_multiplier=ramp_gradient, + net=net, + ) + + def _calculate_spring_comp_force(self, s: float) -> springCompForceBreakdown: + """Hooke's-law spring force resisting shift: F = k * (x0 + s).""" + total_compression = self.initial_compression + s + net = self.spring_coeff_comp * total_compression + return springCompForceBreakdown(compression=s, net=net) + diff --git a/cvtModel/src/cvt_simulator/core/components/secondary_pulley.py b/cvtModel/src/cvt_simulator/core/components/secondary_pulley.py new file mode 100644 index 00000000..29d6a54b --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/components/secondary_pulley.py @@ -0,0 +1,119 @@ +"""Secondary pulley clamping helper (focused, no abstracts). + +Exposes `SecondaryPulley.calculate_axial_clamping_force(shift, torque)` which returns +`(axial_force, SecondaryForceBreakdown)` using existing datatypes. +""" +from typing import Tuple +from cvt_simulator.ramps.theta_ramp import ThetaRamp +import numpy as np +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY +from cvt_simulator.constants.car_specs import MAX_SHIFT +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.data_types import ( + HelixForceBreakdown, + SpringTorsForceBreakdown, + springCompForceBreakdown, + SecondaryForceBreakdown, + PulleyForces, +) +from cvt_simulator.core.components.belt_wrap import BeltWrap + + +class SecondaryPulley: + """Compute secondary pulley clamping (helix torque-reactive + springs). + + Parameters: + spring_coeff_tors: Torsion spring stiffness [N⋅m/rad] + spring_coeff_comp: Compression spring stiffness [N/m] + initial_rotation: Torsion spring preload [rad] + initial_compression: Compression spring preload [m] + helix_ramp: Object providing `theta(shift)`, `dtheta_dx(shift)`, + and `angle_multiplier(shift)` methods (ThetaRamp) + helix_radius: Base helix radius [m] + """ + + def __init__( + self, + spring_coeff_tors: float, + spring_coeff_comp: float, + initial_rotation: float, + initial_compression: float, + helix_ramp: ThetaRamp, + helix_radius: float, + ) -> None: + self.spring_coeff_tors = spring_coeff_tors + self.spring_coeff_comp = spring_coeff_comp + self.initial_rotation = initial_rotation + self.initial_compression = initial_compression + self.helix_ramp = helix_ramp + self.helix_radius = helix_radius + self.cvt = CVT_GEOMETRY + from cvt_simulator.core.components.belt_wrap import BeltWrap + self.belt_wrap = BeltWrap(is_primary=False) + + def calculate_axial_clamping_force(self, state: SystemState, τ: float) -> PulleyForces: + """Return (axial_clamping_force, SecondaryForceBreakdown) from `state` and `tau`. + + Args: + state: Current system state + τ: Transmitted torque at the secondary [N·m] + """ + s = float(np.clip(state.s, 0.0, MAX_SHIFT)) + + helix = self._calculate_helix_force(τ, s) + spring_comp = self._calculate_spring_comp_force(s) + + axial_pulley = helix.net + spring_comp.net + + # belt wrap contribution (use initialized belt_wrap) + belt = self.belt_wrap.axial_centrifugal_force(state) + + axial_total = axial_pulley + belt.axial_belt_force + + breakdown = SecondaryForceBreakdown( + springCompForce=spring_comp, helix_force=helix, net=axial_pulley + ) + + return PulleyForces(pulley_breakdown=breakdown, belt_wrap=belt, net=axial_total) + + def _calculate_helix_force( + self, τ: float, s: float + ) -> HelixForceBreakdown: + """Calculate helix cam force from transmitted torque. + + Uses: F_s,helix,ax = [τ_s + k_s,0(θ_s,0 + θ_s(s)) * dθ_s/ds] / 2 + """ + s = np.clip(s, 0.0, MAX_SHIFT) + + spring_torque_breakdown = self._calculate_spring_tors_torque(s) + angle_multiplier = self.helix_ramp.angle_multiplier(s) + dtheta_ds = self.helix_ramp.dtheta_dx(s) + helix_angle = float(np.arctan2(1.0, self.helix_radius * dtheta_ds)) + + net = (τ + spring_torque_breakdown.net) * dtheta_ds / 2.0 + + return HelixForceBreakdown( + feedbackTorque=τ, + springTorque=spring_torque_breakdown, + angle=helix_angle, + radius=self.helix_radius, + angle_multiplier=angle_multiplier, + net=net, + ) + + def _calculate_spring_comp_force(self, s: float) -> springCompForceBreakdown: + """Calculate compression spring force (static clamping).""" + s = np.clip(s, 0.0, MAX_SHIFT) + total_compression = self.initial_compression + s + net = tm.hookes_law_comp(self.spring_coeff_comp, total_compression) + return springCompForceBreakdown(compression=s, net=net) + + def _calculate_spring_tors_torque(self, s: float) -> SpringTorsForceBreakdown: + """Calculate torsion spring torque from preload + ramp rotation.""" + s = np.clip(s, 0.0, MAX_SHIFT) + rotation_from_shift = self.helix_ramp.theta(s) + total_rotation = self.initial_rotation + rotation_from_shift + net = tm.hookes_law_tors(self.spring_coeff_tors, total_rotation) + return SpringTorsForceBreakdown(rotation=total_rotation, net=net) + diff --git a/cvtModel/src/cvt_simulator/models/external_load_model.py b/cvtModel/src/cvt_simulator/core/components/vehicle_load.py similarity index 95% rename from cvtModel/src/cvt_simulator/models/external_load_model.py rename to cvtModel/src/cvt_simulator/core/components/vehicle_load.py index 7f6f687c..27fcb8f1 100644 --- a/cvtModel/src/cvt_simulator/models/external_load_model.py +++ b/cvtModel/src/cvt_simulator/core/components/vehicle_load.py @@ -7,12 +7,12 @@ GEARBOX_RATIO, ROLLING_RESISTANCE_COEFFICIENT, ) -from cvt_simulator.utils.system_state import SystemState +from cvt_simulator.sim_utils.system_state import SystemState from cvt_simulator.utils.state_computations import ( secondary_pulley_angular_velocity_to_car_velocity, ) -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.models.dataTypes import ExternalLoadForceBreakdown +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm +from cvt_simulator.core.data_types import ExternalLoadForceBreakdown class LoadModel: @@ -51,7 +51,7 @@ def get_breakdown(self, state: SystemState) -> ExternalLoadForceBreakdown: angular velocity ω_s. """ velocity = secondary_pulley_angular_velocity_to_car_velocity( - state.secondary_pulley_angular_velocity + state.ω_s ) rolling_resistance_force = self._calculate_rolling_resistance_force(velocity) diff --git a/cvtModel/src/cvt_simulator/core/data_types.py b/cvtModel/src/cvt_simulator/core/data_types.py new file mode 100644 index 00000000..13c5a87b --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/data_types.py @@ -0,0 +1,235 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum, auto +from typing import Union + +from cvt_simulator.geometry.cvt_geometry import CVTGeometryResult + + + +# ------------------------------------------------- +# Pulley stuff +# ------------------------------------------------- + +@dataclass +class flyweightForceBreakdown: + radius: float + angular_velocity: float + angle: float + + centrifugal_force: float # radius, mass, angular velocity + angle_multiplier: float # tan(angle) + net: float + + +@dataclass +class springCompForceBreakdown: + compression: float + net: float + + +@dataclass +class SpringTorsForceBreakdown: + rotation: float + net: float + + +@dataclass +class HelixForceBreakdown: + feedbackTorque: float + springTorque: SpringTorsForceBreakdown + angle: float + radius: float + + angle_multiplier: float # The 2 * np.tan(angle) * radius + net: float + + +@dataclass +class PrimaryForceBreakdown: + flyweightForce: flyweightForceBreakdown + springForce: springCompForceBreakdown + net: float + + +@dataclass +class SecondaryForceBreakdown: + springCompForce: springCompForceBreakdown + helix_force: HelixForceBreakdown + net: float + +@dataclass +class BeltWrapBreakdown: + wrap_angle: float # Belt wrap angle around pulley [rad] + axial_belt_force: float # Axial force contribution from the belt [N] + +# Overall +@dataclass +class PulleyForces: + pulley_breakdown: Union[PrimaryForceBreakdown, SecondaryForceBreakdown] + belt_wrap: BeltWrapBreakdown + net: float + +@dataclass +class CvtDynamicsBreakdown: + primaryPulleyState: PulleyForces + secondaryPulleyState: PulleyForces + friction: float + acceleration: float + net: float + +# ------------------------------------------------- +# Torque stuff +# ------------------------------------------------- + +@dataclass +class ExternalLoadForceBreakdown: + rolling_resistance_force: float + incline_force: float + drag_force: float + net_force_at_car: float + rolling_resistance_torque_at_secondary: float + incline_torque_at_secondary: float + drag_torque_at_secondary: float + net_torque_at_secondary: float + + +@dataclass +class EngineTorqueBreakdown: + engine_torque: float + engine_speed: float + engine_power: float + +@dataclass +class DrivetrainAccelerationBreakdown: + ω_p_dot: float + ω_s_dot: float + v_b_dot: float + # Engine and load breakdowns included for traceability + engine_breakdown: EngineTorqueBreakdown + external_load_breakdown: ExternalLoadForceBreakdown + # Contact torques that produced these accelerations + tau_p: float + tau_s: float + +# ------------------------------------------------- +# Slip stuff +# ------------------------------------------------- + + +class SlipBranch(Enum): + NO_SLIP = auto() + PRIMARY_SLIP = auto() + SECONDARY_SLIP = auto() + BOTH_SLIP = auto() + + +@dataclass +class NoSlipBreakdown: + r_p: float + r_s: float + r_p_dot: float + r_s_dot: float + tau_engine_over_r_p: float + tau_load_over_r_s: float + primary_inertia_term: float + secondary_inertia_term: float + numerator: float + denominator: float + + +@dataclass +class NoSlipResult: + v_b_dot_ns: float + tau_p_ns: float + tau_s_ns: float + breakdown: NoSlipBreakdown + + +@dataclass +class SlipMetricsResult: + primary_relative_speed: float + secondary_relative_speed: float + primary_slip_direction: float + secondary_slip_direction: float + primary_admissible: bool + secondary_admissible: bool + admissibility: TorqueAdmissibilityResult + no_slip: NoSlipResult + + +@dataclass +class BranchTorqueResult: + branch: SlipBranch + tau_p: float + tau_s: float + + +@dataclass +class ContactTorqueResult: + tau_p: float + tau_s: float + branch: SlipBranch + slip_metrics: SlipMetricsResult + branch_result: BranchTorqueResult + + + +@dataclass +class PrimaryTorqueAdmissibilityBreakdown: + shift_distance: float + wrap_angle: float + effective_radius: float + centroid_radius: float + centroid_radius_rate: float + axial_clamping_force: float + belt_centripetal_term: float + friction_coefficient: float + sheave_half_angle: float + tau_p_stick_limit: float + tau_p_stick_upper: float + tau_p_stick_lower: float + + +@dataclass +class SecondaryTorqueAdmissibilityBreakdown: + shift_distance: float + wrap_angle: float + effective_radius: float + centroid_radius: float + centroid_radius_rate: float + helix_rotation: float + helix_rotation_rate: float + spring_torsion_term: float + spring_comp_term: float + belt_centripetal_term: float + friction_coefficient: float + sheave_half_angle: float + denominator_upper: float + denominator_lower: float + tau_stick_upper: float + tau_stick_lower: float + + +@dataclass +class TorqueAdmissibilityResult: + primary: PrimaryTorqueAdmissibilityBreakdown + secondary: SecondaryTorqueAdmissibilityBreakdown + primary_tau_p_stick_upper: float + primary_tau_p_stick_lower: float + secondary_tau_stick_upper: float + secondary_tau_stick_lower: float + + +# ------------------------------------------------- +# Overall +# ------------------------------------------------- + +@dataclass +class ContactDynamicsBreakdown: + contact: ContactTorqueResult + drivetrain: DrivetrainAccelerationBreakdown + shift: CvtDynamicsBreakdown + geometry: CVTGeometryResult + diff --git a/cvtModel/src/cvt_simulator/core/dynamics/contact_dynamics_model.py b/cvtModel/src/cvt_simulator/core/dynamics/contact_dynamics_model.py new file mode 100644 index 00000000..43bbb400 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/dynamics/contact_dynamics_model.py @@ -0,0 +1,88 @@ +"""Combined contact and drivetrain dynamics bridge. + +This wrapper computes the active contact torques first, then passes the +resolved torques into the drivetrain helper and the secondary torque into the +shift dynamics helper. +""" +from cvt_simulator.core.components.engine import EngineModel +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.core.components.vehicle_load import LoadModel +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, BELT_LENGTH +from cvt_simulator.constants.constants import RUBBER_DENSITY +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.dynamics.drivetrain_dynamics import DrivetrainDynamics +from cvt_simulator.core.dynamics.shift_dynamics import ShiftDynamics +from cvt_simulator.core.slip.contact_torque_solver import ContactTorqueSolver +from cvt_simulator.core.data_types import ContactDynamicsBreakdown +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY + + + +class ContactDynamicsModel: + """Compute contact torques and pass them through drivetrain and shift dynamics.""" + + @staticmethod + def compute_belt_mass() -> float: + """Compute the effective belt mass from geometry and material density.""" + return BELT_CROSS_SECTIONAL_AREA * RUBBER_DENSITY * BELT_LENGTH + + def __init__( + self, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + primary_inertia: float, + secondary_inertia: float, + belt_mass: float, + engine_model: EngineModel, + load_model: LoadModel, + cvt_moving_mass: float = 0.5, + ) -> None: + self.primary_pulley = primary_pulley + self.secondary_pulley = secondary_pulley + self.engine_model = engine_model + self.load_model = load_model + self.contact_torque_solver = ContactTorqueSolver(primary_pulley, secondary_pulley) + self.drivetrain_dynamics = DrivetrainDynamics( + primary_inertia=primary_inertia, + secondary_inertia=secondary_inertia, + belt_mass=belt_mass, + engine_model=engine_model, + load_model=load_model, + ) + self.shift_dynamics = ShiftDynamics( + primary_pulley=primary_pulley, + secondary_pulley=secondary_pulley, + cvt_moving_mass=cvt_moving_mass, + ) + + def get_breakdown(self, state: SystemState) -> ContactDynamicsBreakdown: + """Return the branch-selected contact result and downstream dynamics.""" + tau_engine = self.engine_model.get_torque(state.ω_p) + tau_load = self.load_model.get_breakdown(state).net_torque_at_secondary + + contact = self.contact_torque_solver.solve( + state=state, + tau_engine=tau_engine, + tau_load=tau_load, + I_p=self.drivetrain_dynamics.I_p, + I_s=self.drivetrain_dynamics.I_s, + m_b=self.drivetrain_dynamics.m_b, + ) + drivetrain = self.drivetrain_dynamics.compute_accelerations( + state, + contact.tau_p, + contact.tau_s, + ) + shift = self.shift_dynamics.get_breakdown( + state, + contact.tau_s, + ) + geometry = CVT_GEOMETRY.geometry_from_shift_distance(state.s, state.s_dot) + + return ContactDynamicsBreakdown( + contact=contact, + drivetrain=drivetrain, + shift=shift, + geometry=geometry, + ) diff --git a/cvtModel/src/cvt_simulator/core/dynamics/drivetrain_dynamics.py b/cvtModel/src/cvt_simulator/core/dynamics/drivetrain_dynamics.py new file mode 100644 index 00000000..3c4ebd1c --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/dynamics/drivetrain_dynamics.py @@ -0,0 +1,94 @@ +"""Drivetrain dynamics EOMs. + +Compute rotational accelerations for primary and secondary pulleys and +belt transport acceleration from torques using the equations: + + tau_eng - tau_p = I_p * omega_p_dot + tau_s - tau_load = I_s * omega_s_dot + m_b * v_b_dot = tau_p / r_p_eff(s) - tau_s / r_s_eff(s) + +This module provides a small helper class that accepts inertias and belt +mass and exposes `compute_accelerations(state, tau_p, tau_s)`. +""" +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm +from cvt_simulator.core.components.engine import EngineModel +from cvt_simulator.core.components.vehicle_load import LoadModel +from cvt_simulator.core.data_types import ( + DrivetrainAccelerationBreakdown, + EngineTorqueBreakdown, + ExternalLoadForceBreakdown, +) + + +class DrivetrainDynamics: + """Compute drivetrain accelerations from torques and state. + + This variant queries the provided `EngineModel` and `LoadModel` to obtain + `tau_eng` and `tau_load` directly from the current `state`. + + Args: + primary_inertia: I_p [kg·m²] + secondary_inertia: I_s [kg·m²] + belt_mass: m_b [kg] + engine_model: EngineModel instance to query engine torque + load_model: LoadModel instance to query external load torque + """ + + def __init__( + self, + primary_inertia: float, + secondary_inertia: float, + belt_mass: float, + engine_model: EngineModel, + load_model: LoadModel, + ): + self.I_p = float(primary_inertia) + self.I_s = float(secondary_inertia) + self.m_b = float(belt_mass) + self.engine_model = engine_model + self.load_model = load_model + + def compute_accelerations(self, state: SystemState, τ_p: float, τ_s: float) -> DrivetrainAccelerationBreakdown: + """Compute omega and belt-transport accelerations. + + Args: + state: Current `SystemState` (used for `s` to get effective radii) + tau_p: Torque at primary pulley transmitted to belt [N·m] + tau_s: Torque at secondary pulley transmitted to belt [N·m] + + Returns: + `DrivetrainAccelerations` containing the three derivatives. + """ + s = state.s + + # Use breakdowns returned by the engine/load components directly + engine_bd = self.engine_model.get_breakdown(state.ω_p) + τ_eng = engine_bd.engine_torque + + load_bd = self.load_model.get_breakdown(state) + τ_load = load_bd.net_torque_at_secondary + + # Effective pitch radii from geometry + r_p_eff = tm.primary_effective_radius(s) + r_s_eff = tm.secondary_effective_radius(s) + + # ω_p_dot = (τ_eng - τ_p) / I_p + ω_p_dot = (τ_eng - τ_p) / self.I_p + + # ω_s_dot = (τ_s - τ_load) / I_s + ω_s_dot = (τ_s - τ_load) / self.I_s + + # v_b_dot = (τ_p / r_p_eff - τ_s / r_s_eff) / m_b + v_b_dot = (τ_p / r_p_eff - τ_s / r_s_eff) / self.m_b + + return DrivetrainAccelerationBreakdown( + ω_p_dot=ω_p_dot, + ω_s_dot=ω_s_dot, + v_b_dot=v_b_dot, + engine_breakdown=engine_bd, + external_load_breakdown=load_bd, + tau_p=τ_p, + tau_s=τ_s, + ) + diff --git a/cvtModel/src/cvt_simulator/core/dynamics/shift_dynamics.py b/cvtModel/src/cvt_simulator/core/dynamics/shift_dynamics.py new file mode 100644 index 00000000..bcd76c21 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/dynamics/shift_dynamics.py @@ -0,0 +1,89 @@ +"""Shift dynamics model. + +Computes shift acceleration using primary and secondary pulley states +and net axial force balance. +""" +from cvt_simulator.core.data_types import CvtDynamicsBreakdown +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.sim_utils.system_state import SystemState + +class ShiftDynamics: + """Compute shift acceleration from pulley states and axial force balance. + + This model: + 1. Takes generic pulley models (any implementation) + 2. Gets pulley states from each pulley + 3. Extracts net axial forces + 4. Computes shift acceleration from force balance + 5. Handles friction and system dynamics + + Parameters: + primary_pulley: PrimaryPulleyModel interface instance + secondary_pulley: SecondaryPulleyModel interface instance + cvt_moving_mass: Equivalent mass of shift mechanism [kg] + """ + + def __init__( + self, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + cvt_moving_mass: float = 0.5, + ) -> None: + self.primary_pulley = primary_pulley + self.secondary_pulley = secondary_pulley + self.cvt_moving_mass = cvt_moving_mass + # Shift dynamics now rely on pulley-provided `PulleyForces` + + def get_breakdown( + self, + state: SystemState, + τ_s: float, + ) -> CvtDynamicsBreakdown: + """Compute shift dynamics breakdown. + + Args: + state: Current system state + τ_s: Secondary torque transmitted through CVT [N·m]. + + Returns: + CvtDynamicsBreakdown with all force and acceleration data + """ + # Get primary pulley state (speed-reactive) + primary_forces = self.primary_pulley.calculate_axial_clamping_force(state) + + # Get secondary pulley forces (torque-reactive) + secondary_forces = self.secondary_pulley.calculate_axial_clamping_force(state, τ_s) + + prim_axial = primary_forces.net + sec_axial = secondary_forces.net + net = prim_axial - sec_axial + + friction = self._frictional_force(net, state.s_dot) + + acceleration = (net + friction) / self.cvt_moving_mass + + return CvtDynamicsBreakdown( + primaryPulleyState=primary_forces, + secondaryPulleyState=secondary_forces, + friction=friction, + acceleration=acceleration, + net=net, + ) + + def _frictional_force(self, net_axial_force: float, s_dot: float) -> float: + """Compute frictional force opposing shift motion. + + Args: + net_axial_force: Net force from pulley balance [N] + s_dot: Current shift velocity [m/s] + + Returns: + Frictional force [N] + """ + raw_friction = 20 # TODO: Use calculated/tuned value + friction_magnitude = min(raw_friction, abs(net_axial_force)) + if s_dot > 0: + return -friction_magnitude + return friction_magnitude + diff --git a/cvtModel/src/cvt_simulator/core/slip/branch_algebra.py b/cvtModel/src/cvt_simulator/core/slip/branch_algebra.py new file mode 100644 index 00000000..3db2d8e8 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/slip/branch_algebra.py @@ -0,0 +1,182 @@ +"""Branch-specific algebra moved out of BranchResolver. + +Each function computes the branch kinematics and returns +tau_p, tau_s, v_b_dot for the branch. +""" +import math + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, SHEAVE_ANGLE +from cvt_simulator.constants.constants import RUBBER_ALUMINUM_KINETIC_FRICTION, RUBBER_DENSITY +from cvt_simulator.core.data_types import SlipMetricsResult +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY + + +def primary_slip_algebra( + decision: SlipMetricsResult, + state: SystemState, + tau_load: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, +) -> tuple[float, float]: + s = state.s + s_dot = state.s_dot + v_b = state.v_b + + r_p = decision.no_slip.breakdown.r_p + r_s = decision.no_slip.breakdown.r_s + r_s_dot = decision.no_slip.breakdown.r_s_dot + + r_p_cm = CVT_GEOMETRY.primary_centroid_radius(s) + r_p_cm_dot = CVT_GEOMETRY.primary_outer_radius_time_derivative(s, s_dot) + phi_p = CVT_GEOMETRY.primary_wrap_angle(s) + + F_p_ax = primary_pulley.calculate_axial_clamping_force(state).pulley_breakdown.net + + mu_k = RUBBER_ALUMINUM_KINETIC_FRICTION + beta = SHEAVE_ANGLE / 2.0 + rho_b = RUBBER_DENSITY + A_b = BELT_CROSS_SECTIONAL_AREA + + sigma_p = decision.primary_slip_direction + + numerator = ( + sigma_p * (2.0 * mu_k * F_p_ax * math.tan(beta) - rho_b * A_b * phi_p * r_p_cm_dot * v_b) + - tau_load / r_s + + I_s * r_s_dot * state.ω_s / (r_s ** 2) + ) + denominator = m_b + sigma_p * rho_b * A_b * phi_p * r_p_cm + I_s / (r_s ** 2) + v_b_dot = numerator / denominator + + tau_p = sigma_p * r_p * ( + 2.0 * mu_k * F_p_ax * math.tan(beta) + - rho_b * A_b * phi_p * (r_p_cm * v_b_dot + r_p_cm_dot * v_b) + ) + tau_s = tau_load + I_s * (v_b_dot - r_s_dot * state.ω_s) / r_s + + return tau_p, tau_s + + +def secondary_slip_algebra( + decision: SlipMetricsResult, + state: SystemState, + tau_engine: float, + I_p: float, + m_b: float, + secondary_pulley: SecondaryPulley, +) -> tuple[float, float]: + s = state.s + s_dot = state.s_dot + v_b = state.v_b + + r_p = decision.no_slip.breakdown.r_p + r_p_dot = decision.no_slip.breakdown.r_p_dot + r_s = decision.no_slip.breakdown.r_s + r_s_dot = decision.no_slip.breakdown.r_s_dot + + r_s_cm = CVT_GEOMETRY.secondary_centroid_radius(s) + r_s_cm_dot = CVT_GEOMETRY.secondary_outer_radius_time_derivative(s, s_dot) + phi_s = CVT_GEOMETRY.secondary_wrap_angle(s) + + helix_rotation = secondary_pulley.initial_rotation + secondary_pulley.helix_ramp.theta(s) + helix_rotation_rate = secondary_pulley.helix_ramp.dtheta_dx(s) + spring_torsion_term = secondary_pulley.spring_coeff_tors * helix_rotation * helix_rotation_rate + spring_comp_term = secondary_pulley.spring_coeff_comp * (secondary_pulley.initial_compression + s) + + mu_k = RUBBER_ALUMINUM_KINETIC_FRICTION + beta = SHEAVE_ANGLE / 2.0 + rho_b = RUBBER_DENSITY + A_b = BELT_CROSS_SECTIONAL_AREA + + sigma_s = decision.secondary_slip_direction + den_s = 1.0 - sigma_s * r_s * mu_k * math.tan(beta) * helix_rotation_rate + + traction_common = ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * r_s_cm_dot * v_b + ) + + numerator = ( + tau_engine / r_p + + I_p * r_p_dot * state.ω_p / (r_p ** 2) + - sigma_s * traction_common / den_s + ) + denominator = m_b + I_p / (r_p ** 2) - sigma_s * rho_b * A_b * phi_s * r_s_cm / den_s + v_b_dot = numerator / denominator + + tau_p = tau_engine - I_p * (v_b_dot - r_p_dot * state.ω_p) / r_p + tau_s = sigma_s * r_s * ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * (r_s_cm * v_b_dot + r_s_cm_dot * v_b) + ) / den_s + + return tau_p, tau_s + + +def both_slip_algebra( + decision: SlipMetricsResult, + state: SystemState, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, +) -> tuple[float, float]: + s = state.s + s_dot = state.s_dot + v_b = state.v_b + + r_p = decision.no_slip.breakdown.r_p + r_s = decision.no_slip.breakdown.r_s + + r_p_cm = CVT_GEOMETRY.primary_centroid_radius(s) + r_p_cm_dot = CVT_GEOMETRY.primary_outer_radius_time_derivative(s, s_dot) + r_s_cm = CVT_GEOMETRY.secondary_centroid_radius(s) + r_s_cm_dot = CVT_GEOMETRY.secondary_outer_radius_time_derivative(s, s_dot) + + phi_p = CVT_GEOMETRY.primary_wrap_angle(s) + phi_s = CVT_GEOMETRY.secondary_wrap_angle(s) + F_p_ax = primary_pulley.calculate_axial_clamping_force(state).pulley_breakdown.net + + helix_rotation = secondary_pulley.initial_rotation + secondary_pulley.helix_ramp.theta(s) + helix_rotation_rate = secondary_pulley.helix_ramp.dtheta_dx(s) + spring_torsion_term = secondary_pulley.spring_coeff_tors * helix_rotation * helix_rotation_rate + spring_comp_term = secondary_pulley.spring_coeff_comp * (secondary_pulley.initial_compression + s) + + mu_k = RUBBER_ALUMINUM_KINETIC_FRICTION + beta = SHEAVE_ANGLE / 2.0 + rho_b = RUBBER_DENSITY + A_b = BELT_CROSS_SECTIONAL_AREA + + sigma_p = decision.primary_slip_direction + sigma_s = decision.secondary_slip_direction + + den_s = 1.0 - sigma_s * r_s * mu_k * math.tan(beta) * helix_rotation_rate + if abs(den_s) < 1e-9: + den_s = math.copysign(1e-9, den_s if den_s != 0.0 else 1.0) + + primary_term = sigma_p * (2.0 * mu_k * F_p_ax * math.tan(beta) - rho_b * A_b * phi_p * r_p_cm_dot * v_b) + secondary_term = sigma_s * ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * r_s_cm_dot * v_b + ) / den_s + + numerator = primary_term - secondary_term + denominator = m_b + sigma_p * rho_b * A_b * phi_p * r_p_cm - sigma_s * rho_b * A_b * phi_s * r_s_cm / den_s + v_b_dot = numerator / denominator + + tau_p = sigma_p * r_p * ( + 2.0 * mu_k * F_p_ax * math.tan(beta) + - rho_b * A_b * phi_p * (r_p_cm * v_b_dot + r_p_cm_dot * v_b) + ) + tau_s = sigma_s * r_s * ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * (r_s_cm * v_b_dot + r_s_cm_dot * v_b) + ) / den_s + + return tau_p, tau_s diff --git a/cvtModel/src/cvt_simulator/core/slip/branch_resolver.py b/cvtModel/src/cvt_simulator/core/slip/branch_resolver.py new file mode 100644 index 00000000..862b2814 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/slip/branch_resolver.py @@ -0,0 +1,133 @@ +"""Slip branch resolver. + +Takes the contact-state decision and returns the torques for the selected +branch. The selector provides kinematics and admissibility; this module turns +that into the discrete branch and resolves the branch algebra. +""" + +import math + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, SHEAVE_ANGLE +from cvt_simulator.constants.constants import RUBBER_ALUMINUM_KINETIC_FRICTION, RUBBER_DENSITY +from cvt_simulator.constants.tuning import BELT_STICK_SPEED_THRESHOLD +from cvt_simulator.core.data_types import SlipMetricsResult, BranchTorqueResult, SlipBranch +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm +from cvt_simulator.core.slip.branch_algebra import ( + primary_slip_algebra, + secondary_slip_algebra, + both_slip_algebra, +) + + +class BranchResolver: + """Resolve branch choice into branch torques.""" + + def resolve_branch( + self, + slip_metrics: SlipMetricsResult, + state: SystemState, + tau_engine: float, + tau_load: float, + I_p: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> BranchTorqueResult: + """Return the torques for the selected branch.""" + branch = self._select_branch(slip_metrics) + no_slip = slip_metrics.no_slip + + if branch is SlipBranch.NO_SLIP: + return self._no_slip_branch(no_slip) + if branch is SlipBranch.PRIMARY_SLIP: + return self._primary_slip_branch(branch, slip_metrics, state, tau_load, I_s, m_b, primary_pulley) + if branch is SlipBranch.SECONDARY_SLIP: + return self._secondary_slip_branch(branch, slip_metrics, state, tau_engine, I_p, m_b, secondary_pulley) + return self._both_slip_branch(branch, slip_metrics, state, m_b, primary_pulley, secondary_pulley) + + def _select_branch(self, decision: SlipMetricsResult) -> SlipBranch: + primary_slipping = ( + abs(decision.primary_relative_speed) > BELT_STICK_SPEED_THRESHOLD + or not decision.primary_admissible + ) + secondary_slipping = ( + abs(decision.secondary_relative_speed) > BELT_STICK_SPEED_THRESHOLD + or not decision.secondary_admissible + ) + + if primary_slipping and secondary_slipping: + return SlipBranch.BOTH_SLIP + + if primary_slipping: + return SlipBranch.PRIMARY_SLIP + + if secondary_slipping: + return SlipBranch.SECONDARY_SLIP + + return SlipBranch.NO_SLIP + + def _no_slip_branch(self, no_slip: NoSlipResult) -> BranchTorqueResult: + return BranchTorqueResult( + branch=SlipBranch.NO_SLIP, + tau_p=no_slip.tau_p_ns, + tau_s=no_slip.tau_s_ns, + ) + + def _primary_slip_branch( + self, + branch: SlipBranch, + decision: SlipMetricsResult, + state: SystemState, + tau_load: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, + ) -> BranchTorqueResult: + tau_p, tau_s = primary_slip_algebra(decision, state, tau_load, I_s, m_b, primary_pulley) + return BranchTorqueResult( + branch=branch, + tau_p=tau_p, + tau_s=tau_s, + ) + + def _secondary_slip_branch( + self, + branch: SlipBranch, + decision: SlipMetricsResult, + state: SystemState, + tau_engine: float, + I_p: float, + m_b: float, + secondary_pulley: SecondaryPulley, + ) -> BranchTorqueResult: + tau_p, tau_s = secondary_slip_algebra( + decision, state, tau_engine, I_p, m_b, secondary_pulley + ) + return BranchTorqueResult( + branch=branch, + tau_p=tau_p, + tau_s=tau_s, + ) + + def _both_slip_branch( + self, + branch: SlipBranch, + decision: SlipMetricsResult, + state: SystemState, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> BranchTorqueResult: + tau_p, tau_s = both_slip_algebra( + decision, state, m_b, primary_pulley, secondary_pulley + ) + return BranchTorqueResult( + branch=branch, + tau_p=tau_p, + tau_s=tau_s, + ) \ No newline at end of file diff --git a/cvtModel/src/cvt_simulator/core/slip/contact_torque_solver.py b/cvtModel/src/cvt_simulator/core/slip/contact_torque_solver.py new file mode 100644 index 00000000..83958713 --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/slip/contact_torque_solver.py @@ -0,0 +1,79 @@ +"""Public contact torque orchestration for the slip pipeline. + +This module wraps the no-slip candidate, torque admissibility evaluation, +branch selection, and branch resolution into one call that returns the +selected contact torques and belt acceleration. +""" + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.core.data_types import ContactTorqueResult +from cvt_simulator.sim_utils.system_state import SystemState + +from cvt_simulator.core.slip.branch_resolver import BranchResolver +from cvt_simulator.core.slip.slip_metrics import SlipMetrics +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult, compute_no_slip_candidate +from cvt_simulator.core.slip.torque_admissibility import TorqueAdmissibility + + +class ContactTorqueSolver: + """Resolve contact torques by selecting and solving the active slip branch.""" + + def __init__( + self, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> None: + self.primary_pulley = primary_pulley + self.secondary_pulley = secondary_pulley + self.torque_admissibility = TorqueAdmissibility(primary_pulley, secondary_pulley) + self.branch_selector = SlipMetrics() + self.branch_resolver = BranchResolver() + + def solve( + self, + state: SystemState, + tau_engine: float, + tau_load: float, + I_p: float, + I_s: float, + m_b: float, + ) -> ContactTorqueResult: + """Return the branch-selected contact torques and belt acceleration.""" + no_slip = compute_no_slip_candidate( + state=state, + τ_eng=tau_engine, + τ_load=tau_load, + I_p=I_p, + I_s=I_s, + m_b=m_b, + ) + + admissibility = self.torque_admissibility.get_breakdown(state, no_slip) + + # Contains no slip and admissibility objects + slip_metrics = self.branch_selector.decide_branch( + state=state, + no_slip=no_slip, + admissibility=admissibility, + ) + + branch_result = self.branch_resolver.resolve_branch( + slip_metrics=slip_metrics, + state=state, + tau_engine=tau_engine, + tau_load=tau_load, + I_p=I_p, + I_s=I_s, + m_b=m_b, + primary_pulley=self.primary_pulley, + secondary_pulley=self.secondary_pulley, + ) + + return ContactTorqueResult( + tau_p=branch_result.tau_p, + tau_s=branch_result.tau_s, + branch=branch_result.branch, + slip_metrics=slip_metrics, + branch_result=branch_result, + ) diff --git a/cvtModel/src/cvt_simulator/core/slip/no_slip_candidate.py b/cvtModel/src/cvt_simulator/core/slip/no_slip_candidate.py new file mode 100644 index 00000000..687d99cc --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/slip/no_slip_candidate.py @@ -0,0 +1,82 @@ +"""No-slip candidate dynamics (slip folder). + +Compute the no-slip candidate belt acceleration and corresponding +primary/secondary torques per the derivation: + + v̇_b,ns = (τ_eng/r_p - τ_load/r_s + I_p * ṙ_p * ω_p / r_p**2 + I_s * ṙ_s * ω_s / r_s**2) + / ( m_b + I_p / r_p**2 + I_s / r_s**2 ) + + τ_p,ns = τ_eng - I_p * ( v̇_b,ns - ṙ_p * ω_p ) / r_p + τ_s,ns = τ_load + I_s * ( v̇_b,ns - ṙ_s * ω_s ) / r_s +""" +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.data_types import NoSlipResult, NoSlipBreakdown +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm + + +def compute_no_slip_candidate( + state: SystemState, + τ_eng: float, + τ_load: float, + I_p: float, + I_s: float, + m_b: float, +) -> NoSlipResult: + """Compute no-slip candidate belt acceleration and pulley torques. + + Args: + state: Current SystemState (must contain s, s_dot, ω_p, ω_s) + τ_eng: Engine drive torque [N·m] + τ_load: External load torque at secondary [N·m] + I_p: Primary effective rotational inertia [kg·m²] + I_s: Secondary effective rotational inertia [kg·m²] + m_b: Belt mass (effective) [kg] + + Returns: + NoSlipResult dataclass with v_b_dot_ns, τ_p_ns, τ_s_ns and breakdown + """ + s = state.s + s_dot = state.s_dot + ω_p = state.ω_p + ω_s = state.ω_s + + # Effective radii + r_p = tm.primary_effective_radius(s) + r_s = tm.secondary_effective_radius(s) + + # Time derivatives of radii: dr/dt = (dr/ds) * s_dot + r_p_dot = tm.primary_radius_rate_of_change(s) * s_dot + r_s_dot = tm.secondary_radius_rate_of_change(s) * s_dot + + # Numerator terms + tau_engine_over_r_p = τ_eng / r_p + tau_load_over_r_s = τ_load / r_s + primary_inertia_term = (I_p * r_p_dot * ω_p) / (r_p**2) + secondary_inertia_term = (I_s * r_s_dot * ω_s) / (r_s**2) + + numerator = tau_engine_over_r_p - tau_load_over_r_s + primary_inertia_term + secondary_inertia_term + + # Denominator + denominator = m_b + (I_p / (r_p ** 2)) + (I_s / (r_s ** 2)) + + v_b_dot_ns = numerator / denominator + + # Compute pulley torques under no-slip candidate + tau_p_ns = τ_eng - I_p * (v_b_dot_ns - r_p_dot * ω_p) / r_p + tau_s_ns = τ_load + I_s * (v_b_dot_ns - r_s_dot * ω_s) / r_s + + breakdown = NoSlipBreakdown( + r_p=r_p, + r_s=r_s, + r_p_dot=r_p_dot, + r_s_dot=r_s_dot, + tau_engine_over_r_p=tau_engine_over_r_p, + tau_load_over_r_s=tau_load_over_r_s, + primary_inertia_term=primary_inertia_term, + secondary_inertia_term=secondary_inertia_term, + numerator=numerator, + denominator=denominator, + ) + + return NoSlipResult(v_b_dot_ns=v_b_dot_ns, tau_p_ns=tau_p_ns, tau_s_ns=tau_s_ns, breakdown=breakdown) + diff --git a/cvtModel/src/cvt_simulator/core/slip/slip_metrics.py b/cvtModel/src/cvt_simulator/core/slip/slip_metrics.py new file mode 100644 index 00000000..c637413b --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/slip/slip_metrics.py @@ -0,0 +1,99 @@ +"""Slip branch selection helper. + +Chooses between the four torque-transfer branches: +- no slip +- primary slip +- secondary slip +- both slip + +This module uses the no-slip candidate together with the torque admissibility +limits. It only decides which branch is active and records the relative contact +speeds that determine slip direction. +""" +from __future__ import annotations + +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY +from cvt_simulator.constants.tuning import BELT_STICK_SPEED_THRESHOLD +from cvt_simulator.core.data_types import SlipMetricsResult, TorqueAdmissibilityResult +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm + + +class SlipMetrics: + """Select the active torque-transfer branch. + + This is the new branch-selection implementation. The legacy + `BranchDecider` remains available in `branch_decider.py`. + """ + def __init__(self) -> None: + self.cvt = CVT_GEOMETRY + + def decide_branch( + self, + state: SystemState, + no_slip: NoSlipResult, + admissibility: TorqueAdmissibilityResult, + ) -> SlipMetricsResult: + """Decide which branch is active.""" + primary_relative_speed, secondary_relative_speed = self._relative_contact_speeds(state) + + primary_admissible = ( + admissibility.primary_tau_p_stick_lower + <= no_slip.tau_p_ns + <= admissibility.primary_tau_p_stick_upper + ) + secondary_admissible = ( + admissibility.secondary_tau_stick_lower + <= no_slip.tau_s_ns + <= admissibility.secondary_tau_stick_upper + ) + primary_slip_direction = self._slip_direction( + relative_speed=primary_relative_speed, + tau_ns=no_slip.tau_p_ns, + lower_bound=admissibility.primary_tau_p_stick_lower, + upper_bound=admissibility.primary_tau_p_stick_upper, + ) + secondary_slip_direction = self._slip_direction( + relative_speed=secondary_relative_speed, + tau_ns=no_slip.tau_s_ns, + lower_bound=admissibility.secondary_tau_stick_lower, + upper_bound=admissibility.secondary_tau_stick_upper, + ) + + return SlipMetricsResult( + primary_relative_speed=primary_relative_speed, + secondary_relative_speed=secondary_relative_speed, + primary_slip_direction=primary_slip_direction, + secondary_slip_direction=secondary_slip_direction, + primary_admissible=primary_admissible, + secondary_admissible=secondary_admissible, + admissibility=admissibility, + no_slip=no_slip, + ) + + def _relative_contact_speeds(self, state: SystemState) -> tuple[float, float]: + """Return the contact relative speeds""" + s = state.s + primary_relative_speed = self.cvt.primary_effective_radius(s) * state.ω_p - state.v_b + secondary_relative_speed = state.v_b - self.cvt.secondary_effective_radius(s) * state.ω_s + return primary_relative_speed, secondary_relative_speed + + def _slip_direction( + self, + relative_speed: float, + tau_ns: float, + lower_bound: float, + upper_bound: float, + ) -> float: + # When the contact is moving faster than the stick threshold, direction is set by the + # sign of the relative speed; otherwise it falls back to the torque bounds. + if abs(relative_speed) > BELT_STICK_SPEED_THRESHOLD: + return tm.sgn(relative_speed) + + if tau_ns > upper_bound: + return 1.0 + if tau_ns < lower_bound: + return -1.0 + return 0.0 + diff --git a/cvtModel/src/cvt_simulator/core/slip/torque_admissibility.py b/cvtModel/src/cvt_simulator/core/slip/torque_admissibility.py new file mode 100644 index 00000000..51c603ae --- /dev/null +++ b/cvtModel/src/cvt_simulator/core/slip/torque_admissibility.py @@ -0,0 +1,167 @@ +"""Torque admissibility equations. + +Implements the no-slip admissibility expressions for the primary and +secondary pulleys using the pulley component constants plus the no-slip +belt acceleration result. +""" +from cvt_simulator.core.data_types import ( + PrimaryTorqueAdmissibilityBreakdown, + SecondaryTorqueAdmissibilityBreakdown, + TorqueAdmissibilityResult, +) + +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY +import numpy as np + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, SHEAVE_ANGLE +from cvt_simulator.constants.constants import ( + RUBBER_ALUMINUM_KINETIC_FRICTION, + RUBBER_ALUMINUM_STATIC_FRICTION, + RUBBER_DENSITY, +) +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult + + +class TorqueAdmissibility: + """ + Computes the bounds for the primary and secondary CVT + to determine if no-slip is admissible. + Computed based on the no slip result + """ + + def __init__( + self, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> None: + self.primary_pulley = primary_pulley + self.secondary_pulley = secondary_pulley + self.mu_static = RUBBER_ALUMINUM_STATIC_FRICTION + self.mu_kinetic = RUBBER_ALUMINUM_KINETIC_FRICTION + self.beta = SHEAVE_ANGLE / 2 + self.cvt = CVT_GEOMETRY + + def get_breakdown( + self, + state: SystemState, + no_slip: NoSlipResult, + ) -> TorqueAdmissibilityResult: + """Compute primary and secondary torque admissibility. + + Args: + state: Current system state. + no_slip: No-slip candidate result carrying v_b_dot_ns and torques. + + Returns: + TorqueAdmissibilityResult with explicit term breakdowns. + """ + primary_breakdown = self._primary_breakdown(state, no_slip) + secondary_breakdown = self._secondary_breakdown(state, no_slip) + + return TorqueAdmissibilityResult( + primary=primary_breakdown, + secondary=secondary_breakdown, + primary_tau_p_stick_upper=primary_breakdown.tau_p_stick_upper, + primary_tau_p_stick_lower=primary_breakdown.tau_p_stick_lower, + secondary_tau_stick_upper=secondary_breakdown.tau_stick_upper, + secondary_tau_stick_lower=secondary_breakdown.tau_stick_lower, + ) + + def _primary_breakdown( + self, + state: SystemState, + no_slip: NoSlipResult, + ) -> PrimaryTorqueAdmissibilityBreakdown: + s = state.s + + wrap_angle = self.cvt.primary_wrap_angle(s) + effective_radius = self.cvt.primary_effective_radius(s) + centroid_radius = self.cvt.primary_centroid_radius(s) + centroid_radius_rate = self.cvt.primary_outer_radius_time_derivative(s, state.s_dot) + # Use pulley-only clamping force (exclude belt centrifugal contribution) + axial_clamping_force = self.primary_pulley.calculate_axial_clamping_force(state).pulley_breakdown.net + + belt_centripetal_term = RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * wrap_angle * ( + centroid_radius * no_slip.v_b_dot_ns + centroid_radius_rate * state.v_b + ) + + base_limit = effective_radius * ( + 2.0 * self.mu_static * np.tan(self.beta) * axial_clamping_force + - belt_centripetal_term + ) + tau_p_stick_upper = base_limit + tau_p_stick_lower = -base_limit + + return PrimaryTorqueAdmissibilityBreakdown( + shift_distance=s, + wrap_angle=wrap_angle, + effective_radius=effective_radius, + centroid_radius=centroid_radius, + centroid_radius_rate=centroid_radius_rate, + axial_clamping_force=axial_clamping_force, + belt_centripetal_term=belt_centripetal_term, + friction_coefficient=self.mu_static, + sheave_half_angle=self.beta, + tau_p_stick_limit=base_limit, + tau_p_stick_upper=tau_p_stick_upper, + tau_p_stick_lower=tau_p_stick_lower, + ) + + def _secondary_breakdown( + self, + state: SystemState, + no_slip: NoSlipResult, + ) -> SecondaryTorqueAdmissibilityBreakdown: + s = state.s + + wrap_angle = self.cvt.secondary_wrap_angle(s) + effective_radius = self.cvt.secondary_effective_radius(s) + centroid_radius = self.cvt.secondary_centroid_radius(s) + centroid_radius_rate = self.cvt.secondary_outer_radius_time_derivative(s, state.s_dot) + + helix_rotation = self.secondary_pulley.initial_rotation + self.secondary_pulley.helix_ramp.theta(s) + helix_rotation_rate = self.secondary_pulley.helix_ramp.dtheta_dx(s) + + spring_torsion_term = self.secondary_pulley.spring_coeff_tors * helix_rotation * helix_rotation_rate + spring_comp_term = self.secondary_pulley.spring_coeff_comp * ( + self.secondary_pulley.initial_compression + s + ) + + belt_centripetal_term = RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * wrap_angle * ( + centroid_radius * no_slip.v_b_dot_ns + centroid_radius_rate * state.v_b + ) + + common_numerator = ( + self.mu_static * np.tan(self.beta) * spring_torsion_term + + 2.0 * self.mu_static * np.tan(self.beta) * spring_comp_term + - belt_centripetal_term + ) + numerator = effective_radius * common_numerator + + denominator_upper = 1.0 - effective_radius * self.mu_static * np.tan(self.beta) * helix_rotation_rate + denominator_lower = 1.0 + effective_radius * self.mu_static * np.tan(self.beta) * helix_rotation_rate + + tau_stick_upper = numerator / denominator_upper + tau_stick_lower = -numerator / denominator_lower + + return SecondaryTorqueAdmissibilityBreakdown( + shift_distance=s, + wrap_angle=wrap_angle, + effective_radius=effective_radius, + centroid_radius=centroid_radius, + centroid_radius_rate=centroid_radius_rate, + helix_rotation=helix_rotation, + helix_rotation_rate=helix_rotation_rate, + spring_torsion_term=spring_torsion_term, + spring_comp_term=spring_comp_term, + belt_centripetal_term=belt_centripetal_term, + friction_coefficient=self.mu_static, + sheave_half_angle=self.beta, + denominator_upper=denominator_upper, + denominator_lower=denominator_lower, + tau_stick_upper=tau_stick_upper, + tau_stick_lower=tau_stick_lower, + ) diff --git a/cvtModel/src/cvt_simulator/utils/cvt_ratio_utils.py b/cvtModel/src/cvt_simulator/geometry/cvt_geometry.py similarity index 77% rename from cvtModel/src/cvt_simulator/utils/cvt_ratio_utils.py rename to cvtModel/src/cvt_simulator/geometry/cvt_geometry.py index 4d5dcd83..81893f74 100644 --- a/cvtModel/src/cvt_simulator/utils/cvt_ratio_utils.py +++ b/cvtModel/src/cvt_simulator/geometry/cvt_geometry.py @@ -9,16 +9,28 @@ INITIAL_SHEAVE_DISPLACEMENT, SHEAVE_ANGLE, MAX_SHIFT, + BELT_WIDTH_BOTTOM, + BELT_WIDTH_TOP, ) @dataclass class CVTGeometryResult: + effective_cvt_ratio: float + effective_cvt_ratio_rate_of_change: float + primary_outer_radius: float - secondary_outer_radius: float primary_effective_radius: float + primary_centroid_radius: float + primary_radius_rate_of_change: float + + secondary_outer_radius: float secondary_effective_radius: float - effective_cvt_ratio: float + secondary_centroid_radius: float + secondary_radius_rate_of_change: float + + primary_wrap_angle: float + secondary_wrap_angle: float class CVTGeometry: @@ -59,6 +71,17 @@ def primary_outer_radius(self, d: float) -> float: def primary_effective_radius(self, d: float) -> float: return self.primary_outer_radius(d) - self.h / 2 + def centroid_offset(self) -> float: + """Centroid offset from belt centerline (used by belt centroid radius).""" + return ( + self.h * (BELT_WIDTH_TOP + 2 * BELT_WIDTH_BOTTOM) + / (3 * (BELT_WIDTH_TOP + BELT_WIDTH_BOTTOM)) + ) + + def primary_centroid_radius(self, d: float) -> float: + """Primary centroid radius measured to belt centroid from CVT geometry.""" + return self.primary_effective_radius(d) + self.h / 2 - self.centroid_offset() + # ---------- 2) Secondary radius from primary radius r1 ---------- def _open_form_r_sec(self, r2: float, r1: float) -> float: """ @@ -136,25 +159,62 @@ def secondary_outer_radius(self, d: float) -> float: def secondary_effective_radius(self, d: float) -> float: return self.secondary_outer_radius(d) - self.h / 2 + def secondary_centroid_radius(self, d: float) -> float: + """Secondary centroid radius measured to belt centroid from CVT geometry.""" + return self.secondary_effective_radius(d) + self.h / 2 - self.centroid_offset() + + def wrap_angle(self, primary_radius: float, secondary_radius: float) -> float: + """Half-wrap offset used by primary/secondary wrap calculations.""" + return 2 * asin((secondary_radius - primary_radius) / (2 * self.c2c)) + + def primary_wrap_angle(self, d: float) -> float: + r_p = self.primary_effective_radius(d) + r_s = self.secondary_effective_radius(d) + wrap_offset = self.wrap_angle(r_p, r_s) + if r_p <= r_s: + return pi - wrap_offset + else: + return pi + wrap_offset + + def secondary_wrap_angle(self, d: float) -> float: + r_p = self.primary_effective_radius(d) + r_s = self.secondary_effective_radius(d) + wrap_offset = self.wrap_angle(r_p, r_s) + if r_p <= r_s: + return pi + wrap_offset + else: + return pi - wrap_offset + # ---------- 3) Effective ratio from d ---------- def effective_cvt_ratio(self, d: float) -> float: primary_effective_radius = self.primary_effective_radius(d) secondary_effective_radius = self.secondary_effective_radius(d) return secondary_effective_radius / primary_effective_radius - def geometry_from_shift_distance(self, d: float) -> CVTGeometryResult: - primary_outer_radius = self.primary_outer_radius(d) - secondary_outer_radius = self.secondary_outer_radius(d) + def geometry_from_shift_distance(self, s: float, s_dot: float = 0.0) -> CVTGeometryResult: + primary_outer_radius = self.primary_outer_radius(s) + secondary_outer_radius = self.secondary_outer_radius(s) primary_effective_radius = primary_outer_radius - self.h / 2 secondary_effective_radius = secondary_outer_radius - self.h / 2 effective_cvt_ratio = secondary_effective_radius / primary_effective_radius + # All radiuses have the same rate of change + primary_outer_radius_rate = self.primary_outer_radius_time_derivative(s, s_dot) + secondary_outer_radius_rate = self.secondary_outer_radius_time_derivative(s, s_dot) + effective_cvt_ratio_rate = self.effective_cvt_ratio_time_derivative(s, s_dot) return CVTGeometryResult( primary_outer_radius=primary_outer_radius, - secondary_outer_radius=secondary_outer_radius, primary_effective_radius=primary_effective_radius, + primary_radius_rate_of_change=primary_outer_radius_rate, + secondary_outer_radius=secondary_outer_radius, secondary_effective_radius=secondary_effective_radius, + secondary_radius_rate_of_change=secondary_outer_radius_rate, effective_cvt_ratio=effective_cvt_ratio, + effective_cvt_ratio_rate_of_change=effective_cvt_ratio_rate, + primary_centroid_radius=self.primary_centroid_radius(s), + secondary_centroid_radius=self.secondary_centroid_radius(s), + primary_wrap_angle=self.primary_wrap_angle(s), + secondary_wrap_angle=self.secondary_wrap_angle(s), ) # ---------- 4) Derivatives w.r.t. shift distance d ---------- @@ -304,6 +364,12 @@ def _compute_center_to_center(self) -> float: ) +# Module-level shared CVT geometry instance for global access +# Other modules can import `CVT_GEOMETRY` to query geometry without +# instantiating their own `CVTGeometry` (keeps behavior consistent). +CVT_GEOMETRY = CVTGeometry() + + if __name__ == "__main__": import numpy as np @@ -335,15 +401,17 @@ def _compute_center_to_center(self) -> float: print("=" * 80) print("CVT RATIO TABLE - Shift Distance vs Radii and Ratio") print("=" * 80) - print(f"{'d (m)':<10} {'r1 (m)':<10} {'r2 (m)':<10} {'Ratio':<10} {'di/dd':<12}") - print("-" * 80) + print(f"{ 'd (m)':<10} { 'r1 (m)':<10} { 'r2 (m)':<10} { 'Ratio':<10} { 'di/dd':<12} { 'r1_cent (m)':<14} { 'r2_cent (m)':<14} { 'wrap_p (rad)':<14} { 'wrap_s (rad)':<14}") + print("-" * 140) for d in d_values: result = cvt.geometry_from_shift_distance(d) derivative = cvt._effective_cvt_ratio_shift_derivative(d) print( f"{d:<10.6f} {result.primary_outer_radius:<10.6f} {result.secondary_outer_radius:<10.6f} " - f"{result.effective_cvt_ratio:<10.6f} {derivative:<12.6f}" + f"{result.effective_cvt_ratio:<10.6f} {derivative:<12.6f} " + f"{result.primary_centroid_radius:<14.6f} {result.secondary_centroid_radius:<14.6f} " + f"{result.primary_wrap_angle:<14.6f} {result.secondary_wrap_angle:<14.6f}" ) print("=" * 80) diff --git a/cvtModel/src/cvt_simulator/utils/theoretical_models.py b/cvtModel/src/cvt_simulator/geometry/theoretical_models.py similarity index 78% rename from cvtModel/src/cvt_simulator/utils/theoretical_models.py rename to cvtModel/src/cvt_simulator/geometry/theoretical_models.py index ee624f5b..b6f9cd09 100644 --- a/cvtModel/src/cvt_simulator/utils/theoretical_models.py +++ b/cvtModel/src/cvt_simulator/geometry/theoretical_models.py @@ -1,9 +1,9 @@ import numpy as np -from cvt_simulator.constants.car_specs import CENTER_TO_CENTER -from cvt_simulator.utils.cvt_ratio_utils import CVTGeometry +from cvt_simulator.constants.car_specs import CENTER_TO_CENTER, BELT_HEIGHT, BELT_WIDTH_BOTTOM, BELT_WIDTH_TOP +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY -# Module-level CVTGeometry instance using default constants -_cvt_geometry = CVTGeometry() +# Use shared CVT geometry instance +_cvt_geometry = CVT_GEOMETRY class TheoreticalModels: @@ -78,6 +78,26 @@ def primary_effective_radius(d: float) -> float: def secondary_effective_radius(d: float) -> float: return _cvt_geometry.secondary_effective_radius(d) + @staticmethod + def centroid_offset() -> float: + """Centroid offset from belt centerline (used by belt centroid radius).""" + return ( + BELT_HEIGHT * (BELT_WIDTH_TOP + 2 * BELT_WIDTH_BOTTOM) + / (3 * (BELT_WIDTH_TOP + BELT_WIDTH_BOTTOM)) + ) + + @staticmethod + def primary_centroid_radius(d: float) -> float: + """Primary centroid radius measured to belt centroid from CVT geometry.""" + r_eff = TheoreticalModels.primary_effective_radius(d) + return r_eff + BELT_HEIGHT / 2 - TheoreticalModels.centroid_offset() + + @staticmethod + def secondary_centroid_radius(d: float) -> float: + """Secondary centroid radius measured to belt centroid from CVT geometry.""" + r_eff = TheoreticalModels.secondary_effective_radius(d) + return r_eff + BELT_HEIGHT / 2 - TheoreticalModels.centroid_offset() + @staticmethod def primary_radius_rate_of_change(d: float) -> float: """Get dr_p/dd at current shift position.""" diff --git a/cvtModel/src/cvt_simulator/main.py b/cvtModel/src/cvt_simulator/main.py index 04582917..911b02ad 100644 --- a/cvtModel/src/cvt_simulator/main.py +++ b/cvtModel/src/cvt_simulator/main.py @@ -1,8 +1,7 @@ from typing import Callable, Optional -from cvt_simulator.models.model_initializer import get_models from cvt_simulator.simulation_runner import SimulationRunner -from cvt_simulator.utils.simulation_args import SimulationArgs -from cvt_simulator.utils.frontend_output import FormattedSimulationResult +from cvt_simulator.sim_utils.simulation_args import SimulationArgs +from cvt_simulator.utils.frontend_output import SimulationAnalysisResult def simulate_cvt_model( @@ -10,20 +9,21 @@ def simulate_cvt_model( out_csv: str = "simulation_output.csv", progress_callback: Optional[Callable[[float], None]] = None, ): - system_model = get_models(args) - - simulation_runner = SimulationRunner(system_model, progress_callback) + simulation_runner = SimulationRunner.from_simulation_args( + args, + progress_callback=progress_callback, + ) result = simulation_runner.run_simulation() result.write_csv(out_csv) - formatted = FormattedSimulationResult(result, args) + formatted = SimulationAnalysisResult(result, args) return formatted def main(): formatted = simulate_cvt_model(SimulationArgs()) - formatted.write_formatted_csv("front_end_output.csv") + formatted.write_analysis_csv("front_end_output.csv") if __name__ == "__main__": diff --git a/cvtModel/src/cvt_simulator/models/belt_model.py b/cvtModel/src/cvt_simulator/models/belt_model.py deleted file mode 100644 index eedc3ce5..00000000 --- a/cvtModel/src/cvt_simulator/models/belt_model.py +++ /dev/null @@ -1,175 +0,0 @@ -from cvt_simulator.constants.car_specs import ( - BELT_HEIGHT, - BELT_WIDTH_BOTTOM, - BELT_WIDTH_TOP, -) -from cvt_simulator.constants.tuning import ( - BELT_RELAXATION_GAIN, - BELT_STICK_HYSTERESIS_RATIO, - BELT_STICK_SPEED_RELATIVE_TOLERANCE, - BELT_STICK_SPEED_THRESHOLD, -) -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.models.dataTypes import BeltStateBreakdown - - -class BeltModel: - """Belt transport-speed model for stick/slip regularization.""" - - def __init__( - self, - stick_speed_threshold: float = BELT_STICK_SPEED_THRESHOLD, - stick_speed_relative_tolerance: float = BELT_STICK_SPEED_RELATIVE_TOLERANCE, - stick_hysteresis_ratio: float = BELT_STICK_HYSTERESIS_RATIO, - relaxation_gain: float = BELT_RELAXATION_GAIN, - ): - self.stick_speed_threshold = stick_speed_threshold - self.stick_speed_relative_tolerance = stick_speed_relative_tolerance - self.stick_hysteresis_ratio = stick_hysteresis_ratio - self.relaxation_gain = relaxation_gain - self._last_is_stick: bool | None = None - - def reset_mode_state(self) -> None: - """Reset Schmitt-trigger memory used for stick/slip hysteresis.""" - self._last_is_stick = None - - def _centroid_offset(self) -> float: - return ( - BELT_HEIGHT - * (BELT_WIDTH_TOP + 2 * BELT_WIDTH_BOTTOM) - / (3 * (BELT_WIDTH_TOP + BELT_WIDTH_BOTTOM)) - ) - - def _primary_centroid_radius(self, shift_distance: float) -> float: - r_eff = tm.primary_effective_radius(shift_distance) - return r_eff + BELT_HEIGHT / 2 - self._centroid_offset() - - def _secondary_centroid_radius(self, shift_distance: float) -> float: - r_eff = tm.secondary_effective_radius(shift_distance) - return r_eff + BELT_HEIGHT / 2 - self._centroid_offset() - - def get_stick_speed_tolerances( - self, - primary_belt_speed: float, - secondary_belt_speed: float, - ) -> tuple[float, float]: - speed_scale = max(abs(primary_belt_speed), abs(secondary_belt_speed), 1.0) - stick_enter_tolerance = max( - self.stick_speed_threshold, - self.stick_speed_relative_tolerance * speed_scale, - ) - stick_exit_tolerance = stick_enter_tolerance * self.stick_hysteresis_ratio - return stick_enter_tolerance, stick_exit_tolerance - - def get_kinematic_terms( - self, - state: SystemState, - ) -> tuple[float, float, float, float, float]: - """ - Compute kinematic terms used by both slip and belt-state models. - - Returns: - (v_b_star, T_b, relative_speed, v_p_cm, v_s_cm) - """ - shift_distance = state.shift_distance - r_p_cm = self._primary_centroid_radius(shift_distance) - r_s_cm = self._secondary_centroid_radius(shift_distance) - - v_p_cm = r_p_cm * state.primary_pulley_angular_velocity - v_s_cm = r_s_cm * state.secondary_pulley_angular_velocity - - relative_speed = v_p_cm - v_s_cm - v_b_star = 0.5 * (v_p_cm + v_s_cm) - T_b = 1.0 / self.relaxation_gain - - return v_b_star, T_b, relative_speed, v_p_cm, v_s_cm - - def get_branch_inputs( - self, - state: SystemState, - is_stick_override: bool | None = None, - ) -> tuple[bool, float, float, float, float, float]: - """ - Compute shared branch inputs used by traction bounds and slip logic. - - Returns: - (is_stick, v_b_star, T_b, relative_speed, v_p_cm, v_s_cm) - """ - v_b_star, T_b, relative_speed, v_p_cm, v_s_cm = self.get_kinematic_terms(state) - - if is_stick_override is not None: - is_stick = bool(is_stick_override) - self._last_is_stick = is_stick - return is_stick, v_b_star, T_b, relative_speed, v_p_cm, v_s_cm - - stick_enter_tolerance, stick_exit_tolerance = self.get_stick_speed_tolerances( - v_p_cm, - v_s_cm, - ) - - rel_abs = abs(relative_speed) - if self._last_is_stick is None: - is_stick = rel_abs <= stick_enter_tolerance - elif self._last_is_stick: - is_stick = rel_abs <= stick_exit_tolerance - else: - is_stick = rel_abs <= stick_enter_tolerance - self._last_is_stick = is_stick - - return is_stick, v_b_star, T_b, relative_speed, v_p_cm, v_s_cm - - def get_breakdown( - self, - state: SystemState, - primary_pulley_angular_accel: float, - secondary_pulley_angular_accel: float, - is_stick_override: bool | None = None, - ) -> BeltStateBreakdown: - """Compute belt state breakdown and v_b derivative law.""" - shift_distance = state.shift_distance - shift_velocity = state.shift_velocity - - r_p_cm = self._primary_centroid_radius(shift_distance) - r_s_cm = self._secondary_centroid_radius(shift_distance) - - dr_p_ds = tm.primary_radius_rate_of_change(shift_distance) - dr_s_ds = tm.secondary_radius_rate_of_change(shift_distance) - r_p_cm_dot = dr_p_ds * shift_velocity - r_s_cm_dot = dr_s_ds * shift_velocity - - is_stick, v_b_star, T_b, relative_speed, v_p_cm, v_s_cm = ( - self.get_branch_inputs( - state, - is_stick_override=is_stick_override, - ) - ) - v_b_compatible = v_b_star - - if is_stick: - v_b_dot_primary = ( - r_p_cm_dot * state.primary_pulley_angular_velocity - + r_p_cm * primary_pulley_angular_accel - ) - v_b_dot_secondary = ( - r_s_cm_dot * state.secondary_pulley_angular_velocity - + r_s_cm * secondary_pulley_angular_accel - ) - # Kinematic evolution from pulley accelerations. - v_b_dot_kinematic = 0.5 * (v_b_dot_primary + v_b_dot_secondary) - # Enforce no-slip compatibility in stick so v_b does not drift from v_b*. - v_b_dot = v_b_dot_kinematic + (v_b_compatible - state.v_b) / T_b - else: - v_b_dot = (v_b_star - state.v_b) / T_b - - return BeltStateBreakdown( - is_stick=is_stick, - relative_speed=relative_speed, - primary_belt_speed=v_p_cm, - secondary_belt_speed=v_s_cm, - v_b_star=v_b_star, - T_b=T_b, - v_b=state.v_b, - v_b_compatible=v_b_compatible, - v_b_dot=v_b_dot, - ) diff --git a/cvtModel/src/cvt_simulator/models/cvt_shift_model.py b/cvtModel/src/cvt_simulator/models/cvt_shift_model.py deleted file mode 100644 index eed5a4cb..00000000 --- a/cvtModel/src/cvt_simulator/models/cvt_shift_model.py +++ /dev/null @@ -1,88 +0,0 @@ -from cvt_simulator.models.dataTypes import CvtDynamicsBreakdown -from cvt_simulator.models.pulley.primary_pulley_interface import PrimaryPulleyModel -from cvt_simulator.models.pulley.secondary_pulley_interface import SecondaryPulleyModel -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.models.engine_model import EngineModel - - -class CvtShiftModel: - """ - CVT shift dynamics model using the new generic pulley interface system. - - This model: - 1. Takes generic pulley models (any implementation: physical, PID, lookup, etc.) - 2. Uses total axial forces from each pulley - 3. Determines net shift force and acceleration from the axial force balance - 4. Handles friction and system dynamics - - The abstraction allows swapping pulley implementations without changing - the core shift dynamics. - """ - - def __init__( - self, - engine_model: EngineModel, - primary_pulley: PrimaryPulleyModel, - secondary_pulley: SecondaryPulleyModel, - ): - self.engine_model = engine_model - self.primary_pulley = primary_pulley - self.secondary_pulley = secondary_pulley - self.cvt_moving_mass = 0.5 # TODO: Use constants - - def get_breakdown( - self, state: SystemState, coupling_torque: float - ) -> CvtDynamicsBreakdown: - primary_state, secondary_state = self._get_pulley_states(state, coupling_torque) - - prim_axial = primary_state.forces.axial_force_total - sec_axial = secondary_state.forces.axial_force_total - net = prim_axial - sec_axial - - shift_velocity = state.shift_velocity - friction = self._frictional_force(net, shift_velocity) - - acceleration = (net + friction) / self.cvt_moving_mass - - cvt_ratio = tm.current_effective_cvt_ratio(state.shift_distance) - - return CvtDynamicsBreakdown( - primary_state, - secondary_state, - friction, - acceleration, - cvt_ratio, - net, - ) - - def _get_pulley_states(self, state: SystemState, coupling_torque: float): - """ - Get pulley states from both pulleys using their specific implementations. - - Args: - state: Current system state - coupling_torque: Transmitted torque through CVT [N⋅m] - - Returns: - tuple: (primary_state, secondary_state) as PulleyState objects - """ - # Get primary pulley state (speed-reactive, doesn't need torque) - primary_state = self.primary_pulley.get_pulley_state(state) - - # Calculate CVT ratio for torque scaling to secondary - cvt_ratio = tm.current_effective_cvt_ratio(state.shift_distance) - - # Get secondary pulley state (torque-reactive, needs scaled torque) - secondary_state = self.secondary_pulley.get_pulley_state( - state, torque=coupling_torque * cvt_ratio # Scale torque by CVT ratio - ) - - return primary_state, secondary_state - - def _frictional_force(self, net_axial_force: float, shift_velocity: float) -> float: - raw_friction = 20 # TODO: Update to use calculation - friction_magnitude = min(raw_friction, abs(net_axial_force)) - if shift_velocity > 0: - return -friction_magnitude - return friction_magnitude diff --git a/cvtModel/src/cvt_simulator/models/dataTypes.py b/cvtModel/src/cvt_simulator/models/dataTypes.py deleted file mode 100644 index 154ab9c3..00000000 --- a/cvtModel/src/cvt_simulator/models/dataTypes.py +++ /dev/null @@ -1,239 +0,0 @@ -from dataclasses import dataclass -from typing import Union - - -## Pulley stuff -@dataclass -class flyweightForceBreakdown: - radius: float - angular_velocity: float - angle: float - - centrifugal_force: float # radius, mass, angular velocity - angle_multiplier: float # tan(angle) - net: float - - -@dataclass -class springCompForceBreakdown: - compression: float - net: float - - -@dataclass -class SpringTorsForceBreakdown: - rotation: float - net: float - - -@dataclass -class HelixForceBreakdown: - feedbackTorque: float # TODO: See if this will breakdown - springTorque: SpringTorsForceBreakdown - angle: float - radius: float - - angle_multiplier: float # The 2 * np.tan(angle) * radius - net: float - - -@dataclass -class PrimaryForceBreakdown: - flyweightForce: flyweightForceBreakdown - springForce: springCompForceBreakdown - net: float - - -@dataclass -class SecondaryForceBreakdown: - springCompForce: springCompForceBreakdown - helix_force: HelixForceBreakdown - net: float - - -@dataclass -class PrimaryTorqueNumeratorBreakdown: - clamping_term: float - load_term: float - shift_term: float - net: float - - -@dataclass -class PrimaryTorqueDenominatorBreakdown: - inverse_radius_term: float - inertial_feedback_term: float - net: float - - -@dataclass -class PrimaryTorqueBoundsBreakdown: - tau_lower: float - tau_upper: float - numerator: PrimaryTorqueNumeratorBreakdown - denominator_upper: PrimaryTorqueDenominatorBreakdown - denominator_lower: PrimaryTorqueDenominatorBreakdown - - -@dataclass -class SecondaryTorqueNumeratorBreakdown: - spring_term: float - load_term: float - shift_term: float - net: float - - -@dataclass -class SecondaryTorqueDenominatorBreakdown: - inverse_radius_term: float - helix_feedback_term: float - inertial_feedback_term: float - net: float - - -@dataclass -class SecondaryTorqueBoundsBreakdown: - tau_negative: float - tau_positive: float - numerator: SecondaryTorqueNumeratorBreakdown - denominator_positive: SecondaryTorqueDenominatorBreakdown - denominator_negative: SecondaryTorqueDenominatorBreakdown - - -# All possible pulley breakdown types -PulleyBreakdowns = Union[PrimaryForceBreakdown, SecondaryForceBreakdown] - - -@dataclass -class PulleyForces: - """ - Core outputs that every pulley must provide. - - These values are sufficient to drive the CVT simulation regardless - of the internal mechanism (flyweights, helix, PID, etc.). - """ - - axial_clamping_force: float # Axial force generated by sheave mechanisms [N] - axial_centrifugal_from_belt: ( - float # Axial force contribution from belt centrifugal loading [N] - ) - axial_force_total: float # Total axial force on the pulley [N] - - -@dataclass -class PulleyState: - """ - Complete pulley state including forces and geometric properties. - - Combines the core forces with geometric data needed for system-level - calculations (slip model, shift dynamics, etc.). - """ - - forces: PulleyForces - - # Geometric properties at current shift position - wrap_angle: float # Belt wrap angle around pulley [rad] - radius: float # Effective pitch radius [m] - angular_velocity: float # Pulley angular velocity [rad/s] - angular_position: float # Pulley angular position [rad] - - # Implementation-specific breakdown (Union of all concrete breakdown types) - breakdown: PulleyBreakdowns - - -@dataclass -class CvtDynamicsBreakdown: - primaryPulleyState: PulleyState - secondaryPulleyState: PulleyState - friction: float - acceleration: float - cvt_ratio: float - net: float - - -## External load -@dataclass -class ExternalLoadForceBreakdown: - rolling_resistance_force: float - incline_force: float - drag_force: float - net_force_at_car: float - rolling_resistance_torque_at_secondary: float - incline_torque_at_secondary: float - drag_torque_at_secondary: float - net_torque_at_secondary: float - - @property - def net(self) -> float: - return self.net_torque_at_secondary - - -## Car -@dataclass -class SecondaryPulleyDynamicsBreakdown: - coupling_torque_at_secondary_pulley: float - external_load_torque_at_secondary_pulley: float - external_forces: ExternalLoadForceBreakdown - secondary_pulley_angular_acceleration: float - - -## Engine -@dataclass -class PrimaryPulleyDynamicsBreakdown: - primary_pulley_drive_torque: float - coupling_torque_at_primary_pulley: float - power: float - primary_pulley_angular_velocity: float - primary_pulley_angular_acceleration: float - - -# Slip shenanigans -@dataclass -class SlipBreakdown: - coupling_torque: float - torque_demand: float - tau_upper: float - tau_lower: float - primary_tau_bounds: PrimaryTorqueBoundsBreakdown - secondary_tau_bounds: SecondaryTorqueBoundsBreakdown - effective_cvt_ratio_time_derivative: float - is_slipping: bool - - -@dataclass -class BeltStateBreakdown: - is_stick: bool - relative_speed: float - primary_belt_speed: float - secondary_belt_speed: float - v_b_star: float - T_b: float - v_b: float - v_b_compatible: float - v_b_dot: float - - -## System-level breakdown (single source of truth) -@dataclass -class DrivetrainBreakdown: - """ - Single source of truth for the entire system state. - - Solves the circular dependency problem by: - 1. Calculating all components in the correct dependency order - 2. Providing a single place to access any component's breakdown - 3. Eliminating duplication while maintaining clean interfaces - - Usage: - drivetrain = system_model.get_breakdown(state) - slip_data = drivetrain.belt_slip - primary_data = drivetrain.primary_pulley - secondary_data = drivetrain.secondary_pulley - cvt_data = drivetrain.cvt_dynamics - """ - - belt_slip: SlipBreakdown - belt_state: BeltStateBreakdown - primary_pulley: PrimaryPulleyDynamicsBreakdown - secondary_pulley: SecondaryPulleyDynamicsBreakdown - cvt_dynamics: CvtDynamicsBreakdown diff --git a/cvtModel/src/cvt_simulator/models/engine_model.py b/cvtModel/src/cvt_simulator/models/engine_model.py deleted file mode 100644 index 4e5a931e..00000000 --- a/cvtModel/src/cvt_simulator/models/engine_model.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Callable - - -class EngineModel: - """Pure engine model handling torque curve and power calculations.""" - - def __init__( - self, - torque_curve: Callable[[float], float], # rad/s -> Nm - ): - self.torque_curve = torque_curve - - def get_torque(self, angular_velocity: float) -> float: - """Get the torque output at a given angular velocity.""" - return self.torque_curve(angular_velocity) - - def get_power(self, angular_velocity: float) -> float: - """Get the power output at a given angular velocity.""" - return self.get_torque(angular_velocity) * angular_velocity diff --git a/cvtModel/src/cvt_simulator/models/model_initializer.py b/cvtModel/src/cvt_simulator/models/model_initializer.py deleted file mode 100644 index fc6fb8fb..00000000 --- a/cvtModel/src/cvt_simulator/models/model_initializer.py +++ /dev/null @@ -1,80 +0,0 @@ -from cvt_simulator.models.secondary_pulley_model import SecondaryPulleyModel -from cvt_simulator.models.external_load_model import LoadModel -from cvt_simulator.models.engine_model import EngineModel -from cvt_simulator.models.pulley.primary_pulley_flyweight import PhysicalPrimaryPulley -from cvt_simulator.models.pulley.secondary_pulley_torque_reactive import ( - PhysicalSecondaryPulley, -) -from cvt_simulator.models.cvt_shift_model import CvtShiftModel -from cvt_simulator.constants.engine_specs import safe_torque_curve -from cvt_simulator.utils.conversions import deg_to_rad -from cvt_simulator.utils.simulation_args import SimulationArgs -from cvt_simulator.models.slip_model import SlipModel -from cvt_simulator.models.belt_model import BeltModel -from cvt_simulator.models.primary_pulley_model import PrimaryPulleyModel -from cvt_simulator.models.system_model import SystemModel -from cvt_simulator.models.ramps.piecewise_ramp import PiecewiseRamp -from cvt_simulator.models.ramps.theta_ramp import ThetaRamp -from cvt_simulator.constants.car_specs import HELIX_RADIUS - - -def get_models(args: SimulationArgs): - # Vehicle dynamics - engine_model = EngineModel(torque_curve=safe_torque_curve) - load_model = LoadModel( - car_mass=args.vehicle_weight + args.driver_weight, - incline_angle=deg_to_rad(args.angle_of_incline), - ) - - # CVT dynamics - convert ramp configs to ramp instances - primary_pulley = PhysicalPrimaryPulley( - spring_coeff_comp=args.primary_spring_rate, - initial_compression=args.primary_spring_pretension, - flyweight_mass=args.flyweight_mass, - ramp=PiecewiseRamp.from_config(args.primary_ramp_config), - ) - secondary_pulley = PhysicalSecondaryPulley( - spring_coeff_tors=args.secondary_torsion_spring_rate, - spring_coeff_comp=args.secondary_compression_spring_rate, - initial_rotation=deg_to_rad(args.secondary_rotational_spring_pretension), - initial_compression=args.secondary_linear_spring_pretension, - ramp=ThetaRamp( - PiecewiseRamp.from_config(args.secondary_ramp_config), - HELIX_RADIUS, - ), - ) - - cvt_shift = CvtShiftModel( - engine_model=engine_model, - primary_pulley=primary_pulley, - secondary_pulley=secondary_pulley, - ) - - secondary_pulley_model = SecondaryPulleyModel( - car_mass=args.vehicle_weight + args.driver_weight, - load_model=load_model, - ) - primary_pulley_model = PrimaryPulleyModel(engine_model=engine_model) - - belt_model = BeltModel() - - slip_model = SlipModel( - load_model=load_model, - engine_model=engine_model, - car_mass=args.vehicle_weight + args.driver_weight, - primary_pulley=primary_pulley, - secondary_pulley=secondary_pulley, - primary_pulley_model=primary_pulley_model, - secondary_pulley_model=secondary_pulley_model, - belt_model=belt_model, - ) - - system_model = SystemModel( - slip_model=slip_model, - belt_model=belt_model, - primary_pulley_model=primary_pulley_model, - secondary_pulley_model=secondary_pulley_model, - cvt_shift_model=cvt_shift, - ) - - return system_model diff --git a/cvtModel/src/cvt_simulator/models/primary_pulley_model.py b/cvtModel/src/cvt_simulator/models/primary_pulley_model.py deleted file mode 100644 index 07ea9ec5..00000000 --- a/cvtModel/src/cvt_simulator/models/primary_pulley_model.py +++ /dev/null @@ -1,37 +0,0 @@ -from cvt_simulator.models.dataTypes import PrimaryPulleyDynamicsBreakdown -from cvt_simulator.models.engine_model import EngineModel -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.constants.car_specs import ENGINE_INERTIA - - -class PrimaryPulleyModel: - """Primary-pulley-side angular acceleration model.""" - - def __init__(self, engine_model: EngineModel): - self.engine_model = engine_model - # I_p: primary-side rotational inertia used across coupled dynamics. - self.inertia = ENGINE_INERTIA - - def get_breakdown( - self, state: SystemState, coupling_torque: float - ) -> PrimaryPulleyDynamicsBreakdown: - - # Primary pulley angular velocity is the engine speed - primary_pulley_angular_velocity = state.primary_pulley_angular_velocity - primary_pulley_drive_torque = self.engine_model.get_torque( - primary_pulley_angular_velocity - ) - power = self.engine_model.get_power(primary_pulley_angular_velocity) - - # Torque balance at primary pulley: I * alpha = T_drive - T_coupling - primary_pulley_angular_accel = ( - primary_pulley_drive_torque - coupling_torque - ) / self.inertia - - return PrimaryPulleyDynamicsBreakdown( - primary_pulley_drive_torque=primary_pulley_drive_torque, - coupling_torque_at_primary_pulley=coupling_torque, - power=power, - primary_pulley_angular_velocity=primary_pulley_angular_velocity, - primary_pulley_angular_acceleration=primary_pulley_angular_accel, - ) diff --git a/cvtModel/src/cvt_simulator/models/pulley/primary_pulley_flyweight.py b/cvtModel/src/cvt_simulator/models/pulley/primary_pulley_flyweight.py deleted file mode 100644 index fd6d5f24..00000000 --- a/cvtModel/src/cvt_simulator/models/pulley/primary_pulley_flyweight.py +++ /dev/null @@ -1,323 +0,0 @@ -import numpy as np -from cvt_simulator.models.pulley.primary_pulley_interface import PrimaryPulleyModel -from cvt_simulator.models.pulley.pulley_interface import get_required_kwarg -from cvt_simulator.models.dataTypes import ( - PrimaryForceBreakdown, - PrimaryTorqueBoundsBreakdown, - PrimaryTorqueDenominatorBreakdown, - PrimaryTorqueNumeratorBreakdown, - flyweightForceBreakdown, - springCompForceBreakdown, -) -from cvt_simulator.utils.conversions import inch_to_meter -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.models.ramps import ( - CircularSegment, - LinearSegment, - PiecewiseRamp, -) -from cvt_simulator.constants.car_specs import ( - BELT_CROSS_SECTIONAL_AREA, - MAX_SHIFT, - INITIAL_FLYWEIGHT_RADIUS, - SHEAVE_ANGLE, -) -from cvt_simulator.constants.constants import RUBBER_DENSITY -from cvt_simulator.utils.system_state import SystemState - - -# TODO: Remove this code -def create_default_flyweight_ramp() -> PiecewiseRamp: - """ - Create the default (realistic) flyweight ramp geometry. - - This ramp has: - - Linear start section (engagement) - - Circular finish section (full shift) - - Returns: - PiecewiseRamp with realistic geometry - """ - ramp = PiecewiseRamp() - - # This is the default "Enman" ramp at McMaster baja - - # Linear section: ~0.125 inches at 25 degrees (from horizontal) - line = LinearSegment(length=inch_to_meter(0.125), angle=25) - - # Circular section: remaining length - # Approximating the original curve with a circular arc - circle = CircularSegment( - length=inch_to_meter(1.0), - angle_start=33.4248111826, # degrees (steeper at circle start) - angle_end=20.8067910127, # degrees - quadrant=2, # Mirrored Q3: positive slope while keeping steep-to-gentle shape - ) - - ramp.add_segment(line) - ramp.add_segment(circle) - - return ramp - - -class PhysicalPrimaryPulley(PrimaryPulleyModel): - """ - Flyweight-based primary pulley implementation. - - This is the traditional mechanical CVT primary found in most scooters and ATVs. - Clamping force is generated purely from centrifugal force on flyweights, - modulated by the ramp geometry. - - Physics: - - Flyweights experience centrifugal force: F_c = m * ω² * r - - Ramp converts flyweight motion directly to axial force through dr_f/ds - - Spring opposes shift: F_spring = k * x - - Net clamping: F_clamp = F_flyweight - F_spring - """ - - def __init__( - self, - spring_coeff_comp: float, # N/m - Spring stiffness - initial_compression: float, # m - Spring preload - flyweight_mass: float, # kg - Mass of each flyweight - ramp: PiecewiseRamp, # Flyweight ramp geometry - ): - """ - Initialize physical primary pulley with flyweight mechanism. - - Args: - spring_coeff_comp: Spring compression stiffness [N/m] - initial_compression: Initial spring preload [m] - flyweight_mass: Mass of each flyweight [kg] - ramp: Flyweight ramp geometry - """ - super().__init__() - - self.spring_coeff_comp = spring_coeff_comp - self.initial_compression = initial_compression - self.flyweight_mass = flyweight_mass - self.initial_flyweight_radius = INITIAL_FLYWEIGHT_RADIUS - self.ramp = ramp - self._validate_primary_ramp_admissibility() - - def calculate_axial_clamping_force( - self, state: SystemState, **kwargs - ) -> tuple[float, PrimaryForceBreakdown]: - """ - Calculate mechanism axial clamping force from flyweight force minus spring force. - - Args: - state: Current system state - **kwargs: Not used for physical primary (speed-reactive only) - - Returns: - tuple: (axial_clamping_force, detailed_breakdown) - """ - shift_distance = state.shift_distance - # Primary pulley angular velocity is the engine speed - primary_pulley_angular_velocity = state.primary_pulley_angular_velocity - - # Calculate flyweight centrifugal force on ramp - flyweight_force_breakdown = self._calculate_flyweight_force( - shift_distance, primary_pulley_angular_velocity - ) - - # Calculate spring resistance - spring_force_breakdown = self._calculate_spring_comp_force(shift_distance) - - # Mechanism-only axial clamping force (flyweight pushes, spring resists) - axial_clamping_force = ( - flyweight_force_breakdown.net - spring_force_breakdown.net - ) - - # Package detailed breakdown - breakdown = PrimaryForceBreakdown( - flyweight_force_breakdown, - spring_force_breakdown, - axial_clamping_force, - ) - - return axial_clamping_force, breakdown - - # TODO: Use updated math here - def calculate_torque_bounds( - self, - state: SystemState, - is_stick: bool, - v_b_star: float, - T_b: float, - **kwargs, - ) -> PrimaryTorqueBoundsBreakdown: - """ - Calculate primary traction torque bounds. - - Returns: - PrimaryTorqueBoundsBreakdown with: - - tau_lower / tau_upper limits [N·m] - - numerator term decomposition - - denominator decomposition for upper/lower branches - """ - shift_distance = np.clip(state.shift_distance, 0, MAX_SHIFT) - primary_angular_velocity = state.primary_pulley_angular_velocity - - # Geometry terms - r_eff = self._get_radius(shift_distance) - r_cm = self._get_belt_centroid_radius(shift_distance) - r_dot = self._get_radius_rate_of_change(shift_distance) * state.shift_velocity - phi = self._get_wrap_angle(shift_distance) - beta = SHEAVE_ANGLE / 2 - - # Dynamic terms - tau_eng = get_required_kwarg(kwargs, "engine_drive_torque") - I_p = get_required_kwarg(kwargs, "primary_inertia") - - # This must be ONLY the mechanism clamping force term, not total force with belt corrections - axial_clamping_force, _ = self.calculate_axial_clamping_force(state) - - belt_mass_term = RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * r_cm * phi - μ_branch = self.μ_static if is_stick else self.μ_kinetic - clamping_term = 2.0 * μ_branch * np.tan(beta) * axial_clamping_force - - if is_stick: - load_term = -belt_mass_term * ((r_cm**2 * tau_eng) / I_p) - shift_term = -belt_mass_term * ( - 2.0 * r_cm * r_dot * primary_angular_velocity - ) - numerator_net = r_eff * (clamping_term + load_term + shift_term) - - denominator_feedback = r_eff * belt_mass_term * ((r_cm**2) / I_p) - upper_denominator = 1.0 - denominator_feedback - lower_denominator = 1.0 + denominator_feedback - - tau_upper = numerator_net / upper_denominator - tau_lower = -numerator_net / lower_denominator - - denominator_upper_breakdown = PrimaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - inertial_feedback_term=-denominator_feedback, - net=upper_denominator, - ) - - denominator_lower_breakdown = PrimaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - inertial_feedback_term=denominator_feedback, - net=lower_denominator, - ) - else: - load_term = -belt_mass_term * (r_cm * ((v_b_star - state.v_b) / T_b)) - shift_term = -belt_mass_term * (r_dot * state.v_b) - numerator_net = r_eff * (clamping_term + load_term + shift_term) - - tau_upper = numerator_net - tau_lower = -numerator_net - - denominator_upper_breakdown = PrimaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - inertial_feedback_term=0.0, - net=1.0, - ) - - denominator_lower_breakdown = PrimaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - inertial_feedback_term=0.0, - net=1.0, - ) - - numerator_breakdown = PrimaryTorqueNumeratorBreakdown( - clamping_term=r_eff * clamping_term, - load_term=r_eff * load_term, - shift_term=r_eff * shift_term, - net=numerator_net, - ) - - return PrimaryTorqueBoundsBreakdown( - tau_lower=tau_lower, - tau_upper=tau_upper, - numerator=numerator_breakdown, - denominator_upper=denominator_upper_breakdown, - denominator_lower=denominator_lower_breakdown, - ) - - # Private helper methods for force calculations - - def _calculate_flyweight_force( - self, shift_distance: float, angular_velocity: float - ) -> flyweightForceBreakdown: - """Calculate flyweight centrifugal force and conversion through ramp.""" - # Clamp shift distance to valid range - # TODO: Remove extra clamp - shift_distance = np.clip(shift_distance, 0, MAX_SHIFT) - - # Height is modeled as additional radial displacement from center. - flyweight_radius = self.initial_flyweight_radius + self.ramp.height( - shift_distance - ) - - # Centrifugal force on flyweight: F = m * ω² * r - centrifugal_force = tm.centrifugal_force( - self.flyweight_mass, - angular_velocity, - flyweight_radius, - ) - - # Ramp derivative dr_f/ds at current position. - ramp_gradient = self.ramp.slope(shift_distance) - - # F_p,ax = m_f * omega_p^2 * (r_f,0 + r_f(s)) * dr_f/ds - net = centrifugal_force * ramp_gradient - - # Retain angle output for debug/plots while using derivative for force law. - angle = np.arctan(ramp_gradient) - - return flyweightForceBreakdown( - radius=flyweight_radius, - angular_velocity=angular_velocity, - angle=angle, - centrifugal_force=centrifugal_force, - angle_multiplier=ramp_gradient, - net=net, - ) - - def _validate_primary_ramp_admissibility(self) -> None: - """Validate primary ramp profile constraints for r_f(s).""" - if not self.ramp.segments: - raise ValueError("Primary ramp must contain at least one segment") - - for segment in self.ramp.segments: - sample_points = [ - segment.x_start, - (segment.x_start + segment.x_end) / 2, - segment.x_end, - ] - for x in sample_points: - slope = self.ramp.slope(x) - if not np.isfinite(slope): - raise ValueError( - f"Primary ramp slope must be finite on [0, {MAX_SHIFT}], got {slope} at x={x}." - ) - if slope < 0: - raise ValueError( - f"Primary ramp slope must be non-negative on [0, {MAX_SHIFT}], got {slope} at x={x}." - ) - - angle_deg = np.degrees(np.arctan(slope)) - if angle_deg < 0 or angle_deg >= 90: - raise ValueError( - "Primary ramp angle must be in [0, 90) degrees from horizontal; " - f"got {angle_deg} degrees at x={x}." - ) - - def _calculate_spring_comp_force( - self, shift_distance: float - ) -> springCompForceBreakdown: - """Calculate spring resistance force (opposes shifting).""" - # Total compression = preload + shift distance - total_compression = self.initial_compression + shift_distance - - # Hooke's law: F = k * x - net = tm.hookes_law_comp(self.spring_coeff_comp, total_compression) - - return springCompForceBreakdown( - compression=shift_distance, - net=net, - ) diff --git a/cvtModel/src/cvt_simulator/models/pulley/primary_pulley_interface.py b/cvtModel/src/cvt_simulator/models/pulley/primary_pulley_interface.py deleted file mode 100644 index 370d2d03..00000000 --- a/cvtModel/src/cvt_simulator/models/pulley/primary_pulley_interface.py +++ /dev/null @@ -1,42 +0,0 @@ -from cvt_simulator.models.pulley.pulley_interface import PulleyModel -from pyparsing import ABC -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm - - -class PrimaryPulleyModel(PulleyModel, ABC): - """ - Abstract base for primary (engine-side) pulley implementations. - - Primary pulleys typically: - - Run at primary pulley speed (engine speed) - - Generate clamping force from centrifugal mechanisms (flyweights) or active control - - Start at large radius (low ratio) and shift to small radius (high ratio) - - Concrete geometric methods are provided based on CVT geometry, - only clamping force calculation is left to specific implementations. - """ - - def _get_wrap_angle(self, shift_distance: float) -> float: - """Get primary belt wrap angle at current shift position [rad].""" - return tm.primary_wrap_angle(shift_distance) - - def _get_radius(self, shift_distance: float) -> float: - """Get primary effective pitch radius at current shift position [m].""" - return tm.primary_effective_radius(shift_distance) - - def _get_radius_rate_of_change(self, shift_distance: float): - """Get dr/dt at current shift position [m/m].""" - return tm.primary_radius_rate_of_change(shift_distance) - - def _get_angular_velocity(self, state: SystemState) -> float: - """Get primary pulley angular velocity [rad/s].""" - return state.primary_pulley_angular_velocity - - def _get_angular_position(self, state: SystemState) -> float: - """Get primary pulley angular position (engine position) [rad]. - - Note: Angular position is not part of the core 4 DOF state. - This method is kept for compatibility but should not be used for ODE integration. - """ - return 0.0 # Placeholder - position is not integrated as part of core dynamics diff --git a/cvtModel/src/cvt_simulator/models/pulley/pulley_interface.py b/cvtModel/src/cvt_simulator/models/pulley/pulley_interface.py deleted file mode 100644 index 08a1a7d1..00000000 --- a/cvtModel/src/cvt_simulator/models/pulley/pulley_interface.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -Abstract interfaces for CVT pulley models. - -This module defines the core contracts that all pulley implementations must satisfy, -allowing different control strategies (physical models, PID controllers, lookup tables, etc.) -to be swapped without changing the rest of the simulation. - -Key Design Principles: -- Each pulley must provide: clamping force and max torque -- Implementation details (flyweights, helix, PID, etc.) are encapsulated -- Breakdowns provide detailed internal state for debugging/visualization -- **kwargs pattern allows flexible, future-proof parameter passing - -Design Pattern Notes: -- **kwargs: Maximum flexibility for implementation-specific parameters -- Common kwargs: 'torque', 'target_rpm', 'target_ratio', 'load_factor' -- Use get_kwarg() helper for safe extraction with defaults -- Trade-off: Flexibility vs type safety (document expected kwargs well) -""" - -from abc import ABC, abstractmethod -from typing import Any -import numpy as np -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.constants.car_specs import ( - SHEAVE_ANGLE, - BELT_CROSS_SECTIONAL_AREA, - BELT_HEIGHT, - BELT_WIDTH_TOP, - BELT_WIDTH_BOTTOM, -) -from cvt_simulator.constants.constants import ( - RUBBER_DENSITY, - RUBBER_ALUMINUM_STATIC_FRICTION, - RUBBER_ALUMINUM_KINETIC_FRICTION, -) -from cvt_simulator.models.dataTypes import PulleyState, PulleyForces, PulleyBreakdowns - - -def get_kwarg(kwargs: dict[str, Any], key: str, default: Any = None) -> Any: - """ - Helper function to safely extract optional kwargs with defaults. - - Usage in implementations: - # Optional with default - load_factor = get_kwarg(kwargs, 'load_factor', 1.0) - - # Optional with None default - target_rpm = get_kwarg(kwargs, 'target_rpm') - - Args: - kwargs: The kwargs dict from calculate_axial_clamping_force - key: The parameter name to extract - default: Default value if key not found (default: None) - - Returns: - The value from kwargs or the default - """ - return kwargs.get(key, default) - - -def get_required_kwarg(kwargs: dict[str, Any], key: str, error_msg: str = None) -> Any: - """ - Helper function to extract required kwargs with validation. - - Usage in implementations: - # Required parameter with auto-generated error - torque = get_required_kwarg(kwargs, 'torque') - - # Required with custom error message - torque = get_required_kwarg( - kwargs, 'torque', - error_msg="PhysicalSecondaryPulley requires 'torque' for torque-reactive operation" - ) - - Args: - kwargs: The kwargs dict from calculate_axial_clamping_force - key: The parameter name to extract - error_msg: Custom error message (optional, auto-generated if not provided) - - Returns: - The value from kwargs - - Raises: - ValueError: If key is not in kwargs - """ - if key not in kwargs: - if error_msg is None: - error_msg = f"Required parameter '{key}' not provided in kwargs" - raise ValueError(error_msg) - - return kwargs[key] - - -class PulleyModel(ABC): - """ - Abstract base class for all pulley control strategies. - - Subclasses implement specific mechanisms: - - PhysicalPrimaryPulley: flyweight-based (centrifugal force on ramp) - - PhysicalSecondaryPulley: helix-based (torque feedback through cam) - - PIDPrimaryPulley: electronic control with target RPM - - LookupTablePulley: pre-computed force maps - - etc. - - The abstraction allows the simulation to work with any mechanism that - can provide the required outputs (clamping force and max torque). - """ - - def __init__(self): - """Initialize pulley model with V-belt friction coefficients.""" - # Calculate friction coefficient with V-belt wedging effect - # The sheave angle enhances friction through wedging action - self.μ_static = RUBBER_ALUMINUM_STATIC_FRICTION - self.μ_kinetic = RUBBER_ALUMINUM_KINETIC_FRICTION - # Backward compatibility for any existing model code using self.μ. - self.μ = self.μ_static - - @abstractmethod - def calculate_axial_clamping_force( - self, state: SystemState, **kwargs - ) -> tuple[float, PulleyBreakdowns]: - """ - Calculate the axial clamping force pushing pulley halves together. - - This is the core mechanism-specific calculation. - Supports different implementations (e.g., flyweights, helix, PID control). - - Args: - state: Current system state (shift position, velocities, etc.) - **kwargs: Implementation-specific parameters, may include: - - torque (float): Torque at this pulley [N⋅m] - * Primary: typically unused (flyweights are speed-reactive) - * Secondary (helix): transmitted torque = primary_torque * cvt_ratio - * Secondary (non-reactive): ignored - - target_rpm (float): Target engine RPM for PID control - - target_ratio (float): Target CVT ratio for active control - - load_factor (float): Load-based shift correction - - Any future parameters needed by new implementations - - Returns: - tuple: (axial_clamping_force, breakdown) - - axial_clamping_force: Axial force from pulley hardware [N] - - breakdown: Implementation-specific detailed breakdown - """ - pass - - def axial_centrifugal_from_belt(self, state: SystemState) -> float: - """ - Calculate centrifugal belt contribution projected into axial direction. - - Implements: - F_c,ax = rho_b * A_b * omega^2 * r_cm^2 * phi / (2 * tan(beta)) - - where beta is the sheave half-angle. - """ - shift_distance = state.shift_distance - wrap_angle = self._get_wrap_angle(shift_distance) - belt_velocity = state.v_b - beta = SHEAVE_ANGLE / 2 - - return ( - RUBBER_DENSITY - * BELT_CROSS_SECTIONAL_AREA - * belt_velocity**2 - * wrap_angle - / (2 * np.tan(beta)) - ) - - def _get_belt_centroid_radius(self, shift_distance: float) -> float: - """Get belt mass-centroid radius r_cm at current shift position [m].""" - # Delta from trapezoidal belt cross-section centroid (measured from outer face). - delta_r_cm = ( - BELT_HEIGHT - * (BELT_WIDTH_TOP + 2 * BELT_WIDTH_BOTTOM) - / (3 * (BELT_WIDTH_TOP + BELT_WIDTH_BOTTOM)) - ) - r_out = self._get_radius(shift_distance) + BELT_HEIGHT / 2 - return r_out - delta_r_cm - - def calculate_integrated_normal_load(self, axial_force_total: float) -> float: - """ - Get integrated normal load over wrap from total axial force (N_phi). - - Uses N_phi = 2 * F_ax * tan(beta), where beta is sheave half-angle. - """ - return 2 * axial_force_total * np.tan(SHEAVE_ANGLE / 2) - - @abstractmethod - def calculate_torque_bounds( - self, - state: SystemState, - is_stick: bool, - v_b_star: float, - T_b: float, - **kwargs, - ) -> tuple[float, float]: - """ - Calculate maximum transferable torque before belt slip. - - Uses Capstan equation (or Eytelwein formula) in an axial-load formulation. - The pulley calculates its own axial force internally based on - current operating conditions. - - The limiting torque depends on: - - Belt-pulley friction (enhanced by V-groove wedging) - - Wrap angle (more wrap = more capacity) - - Total axial loading (sheave clamp + belt centrifugal contribution) - - Effective radius - - Args: - state: Current system state - is_stick: True for stick branch, False for slip branch. - v_b_star: Branch driving speed reference. - T_b: Branch time constant. - **kwargs: Model-specific parameters (for example external load torque, - engine torque, and side inertia terms). - - Returns: - max_torque: Maximum torque capacity [N⋅m] - """ - pass - - def get_pulley_state(self, state: SystemState, **kwargs) -> PulleyState: - """ - Calculate complete pulley state (main entry point). - - This orchestrates the three core calculations in sequence: - 1. Calculate axial clamping force from the pulley mechanism - 2. Calculate axial centrifugal belt contribution - 3. Form total axial force - 4. Calculate max torque (Capstan equation) - - Args: - state: Current system state - **kwargs: Implementation-specific parameters (see calculate_axial_clamping_force) - - Returns: - PulleyState with all forces, geometry, and detailed breakdown - """ - # Step 1: Get mechanism-generated axial clamping force and breakdown - axial_clamping_force, breakdown = self.calculate_axial_clamping_force( - state, **kwargs - ) - - # Step 2: Axial centrifugal belt contribution - axial_centrifugal_from_belt = self.axial_centrifugal_from_belt(state) - - # Step 3: Total axial force - axial_force_total = axial_clamping_force + axial_centrifugal_from_belt - - # Get geometric properties - wrap_angle = self._get_wrap_angle(state.shift_distance) - radius = self._get_radius(state.shift_distance) - angular_velocity = self._get_angular_velocity(state) - angular_position = self._get_angular_position(state) - - # Package into PulleyForces - forces = PulleyForces( - axial_clamping_force=axial_clamping_force, - axial_centrifugal_from_belt=axial_centrifugal_from_belt, - axial_force_total=axial_force_total, - ) - - # Return complete state - return PulleyState( - forces=forces, - wrap_angle=wrap_angle, - radius=radius, - angular_velocity=angular_velocity, - angular_position=angular_position, - breakdown=breakdown, - ) - - # Geometric helper methods - implemented by Primary/Secondary base classes - @abstractmethod - def _get_wrap_angle(self, shift_distance: float) -> float: - """Get belt wrap angle at current shift position [rad].""" - pass - - @abstractmethod - def _get_radius(self, shift_distance: float) -> float: - """Get effective pitch radius at current shift position [m].""" - pass - - @abstractmethod - def _get_radius_rate_of_change(self, shift_distance: float) -> float: - """Get dr/dt at current shift position [m/m].""" - pass - - @abstractmethod - def _get_angular_velocity(self, state: SystemState) -> float: - """Get pulley angular velocity [rad/s].""" - pass - - @abstractmethod - def _get_angular_position(self, state: SystemState) -> float: - """Get pulley angular position [rad].""" - pass diff --git a/cvtModel/src/cvt_simulator/models/pulley/secondary_pulley_interface.py b/cvtModel/src/cvt_simulator/models/pulley/secondary_pulley_interface.py deleted file mode 100644 index 005a91db..00000000 --- a/cvtModel/src/cvt_simulator/models/pulley/secondary_pulley_interface.py +++ /dev/null @@ -1,43 +0,0 @@ -from cvt_simulator.models.pulley.pulley_interface import PulleyModel -from pyparsing import ABC -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm - - -class SecondaryPulleyModel(PulleyModel, ABC): - """ - Abstract base for secondary (driven-side) pulley implementations. - - Secondary pulleys typically: - - Run at secondary pulley speed (wheel speed / gearbox) - - Generate clamping force from torque feedback (helix) or active control - - Start at small radius (low ratio) and shift to large radius (high ratio) - - Must react to torque to provide back-pressure for shifting - - Concrete geometric methods are provided based on CVT geometry, - only clamping force calculation is left to specific implementations. - """ - - def _get_wrap_angle(self, shift_distance: float) -> float: - """Get secondary belt wrap angle at current shift position [rad].""" - return tm.secondary_wrap_angle(shift_distance) - - def _get_radius(self, shift_distance: float) -> float: - """Get secondary effective pitch radius at current shift position [m].""" - return tm.secondary_effective_radius(shift_distance) - - def _get_radius_rate_of_change(self, shift_distance): - """Get dr/dt at current shift position [m/m].""" - return tm.secondary_radius_rate_of_change(shift_distance) - - def _get_angular_velocity(self, state: SystemState) -> float: - """Get secondary pulley angular velocity [rad/s].""" - return state.secondary_pulley_angular_velocity - - def _get_angular_position(self, state: SystemState) -> float: - """Get secondary pulley angular position (wheel position / gearbox) [rad]. - - Note: Angular position is not part of the core 4 DOF state. - This method is kept for compatibility but should not be used for ODE integration. - """ - return 0.0 # Placeholder - position is not integrated as part of core dynamics diff --git a/cvtModel/src/cvt_simulator/models/pulley/secondary_pulley_torque_reactive.py b/cvtModel/src/cvt_simulator/models/pulley/secondary_pulley_torque_reactive.py deleted file mode 100644 index ec117bb0..00000000 --- a/cvtModel/src/cvt_simulator/models/pulley/secondary_pulley_torque_reactive.py +++ /dev/null @@ -1,322 +0,0 @@ -import numpy as np -from cvt_simulator.models.pulley.secondary_pulley_interface import SecondaryPulleyModel -from cvt_simulator.models.pulley.pulley_interface import get_required_kwarg -from cvt_simulator.models.dataTypes import ( - HelixForceBreakdown, - SecondaryForceBreakdown, - SecondaryTorqueBoundsBreakdown, - SecondaryTorqueDenominatorBreakdown, - SecondaryTorqueNumeratorBreakdown, - SpringTorsForceBreakdown, - springCompForceBreakdown, -) -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.constants.car_specs import ( - BELT_CROSS_SECTIONAL_AREA, - MAX_SHIFT, - HELIX_RADIUS, - SHEAVE_ANGLE, -) -from cvt_simulator.constants.constants import RUBBER_DENSITY -from cvt_simulator.models.ramps import LinearSegment, PiecewiseRamp, ThetaRamp -from cvt_simulator.utils.system_state import SystemState - - -def create_default_helix_ramp() -> ThetaRamp: - """ - Create the default helix geometry as a theta ramp. - - New convention: - - s is axial shift distance [m] - - u(s) = r_h * theta(s) is circumferential displacement [m] - - tan(alpha_s) = 1 / (r_h * dtheta/ds) - - equivalently: du/ds = cot(alpha_s) - - Segment angles passed to ThetaRamp are helix angles from circumferential. - Default helix angle is alpha_s = 36°. - - Returns: - ThetaRamp using a PiecewiseRamp that stores u(s) - """ - helix_angle_deg = 36.0 - angle_ramp = PiecewiseRamp() - angle_ramp.add_segment(LinearSegment(length=MAX_SHIFT, angle=helix_angle_deg)) - return ThetaRamp(angle_ramp, HELIX_RADIUS) - - -class PhysicalSecondaryPulley(SecondaryPulleyModel): - """ - Helix-based secondary pulley implementation. - - This is the traditional torque-reactive secondary found in most mechanical CVTs. - Torque transmitted through the CVT causes the helix cam to rotate, converting - rotational torque into axial clamping force through the cam angle. - - Physics: - - Transmitted torque causes cam rotation: τ → θ - - Helix cam converts torque to axial force: F_axial = τ / (r * tan(helix_angle)) - - Torsion spring resists cam rotation and adds base clamping - - Compression spring adds static clamping force - - Net clamping: F_clamp = F_helix + F_torsion_spring + F_comp_spring - - This provides automatic torque feedback: more load = more clamping = better grip. - """ - - def __init__( - self, - spring_coeff_tors: float, # Nm/rad - Torsion spring stiffness - spring_coeff_comp: float, # N/m - Compression spring stiffness - initial_rotation: float, # rad - Torsion spring preload - initial_compression: float, # m - Compression spring preload - ramp: ThetaRamp, # Helix cam geometry - ): - """ - Initialize physical secondary pulley with helix mechanism. - - Args: - spring_coeff_tors: Torsion spring stiffness [N⋅m/rad] - spring_coeff_comp: Compression spring stiffness [N/m] - initial_rotation: Initial torsion spring preload [rad] - initial_compression: Initial compression spring preload [m] - ramp: Helix cam geometry - """ - super().__init__() - - self.spring_coeff_tors = spring_coeff_tors - self.spring_coeff_comp = spring_coeff_comp - self.initial_rotation = initial_rotation - self.initial_compression = initial_compression - self.helix_radius = HELIX_RADIUS - self.theta_ramp = ramp - - def calculate_axial_clamping_force( - self, state: SystemState, **kwargs - ) -> tuple[float, SecondaryForceBreakdown]: - """ - Calculate mechanism axial clamping force from helix torque feedback + spring forces. - - Implements equation 8.16: - F_s,ax(s, τ_s) = [τ_s + k_s,0(θ_s,0 + θ_s(s)) * dθ_s/ds] / 2 + k_s,x(x_s,0 - s) - - Args: - state: Current system state - **kwargs: Expected key: - - torque (float): Transmitted torque through CVT [N⋅m] - - Returns: - tuple: (axial_clamping_force, detailed_breakdown) - """ - shift_distance = np.clip(state.shift_distance, 0, MAX_SHIFT) - - # Extract torque from kwargs (required for torque-reactive secondary) - torque = get_required_kwarg( - kwargs, - "torque", - error_msg=( - "PhysicalSecondaryPulley requires 'torque' parameter in kwargs. " - "This is a torque-reactive pulley that needs transmitted torque to calculate clamping force." - ), - ) - - # Calculate helix force from torque feedback (Eq. 8.16 helix term) - helix_force_breakdown = self._calculate_helix_force(torque, shift_distance) - - # Calculate compression spring force (Eq. 8.16 axial spring term) - spring_comp_force_breakdown = self._calculate_spring_comp_force(shift_distance) - - # Total axial clamping force - axial_clamping_force = ( - helix_force_breakdown.net + spring_comp_force_breakdown.net - ) - - breakdown = SecondaryForceBreakdown( - spring_comp_force_breakdown, - helix_force_breakdown, - axial_clamping_force, - ) - - return axial_clamping_force, breakdown - - def calculate_torque_bounds( - self, - state: SystemState, - is_stick: bool, - v_b_star: float, - T_b: float, - **kwargs, - ) -> SecondaryTorqueBoundsBreakdown: - """ - Calculate secondary traction torque bounds. - - Returns: - SecondaryTorqueBoundsBreakdown with: - - tau_negative / tau_positive limits [N·m] - - numerator term decomposition - - denominator decomposition for both +/- branches - """ - shift_distance = np.clip(state.shift_distance, 0, MAX_SHIFT) - angular_velocity = self._get_angular_velocity(state) - - # Geometry terms - r_eff = self._get_radius(shift_distance) - r_cm = self._get_belt_centroid_radius(shift_distance) - cvt_ratio = tm.current_effective_cvt_ratio(shift_distance) - - # _get_radius_rate_of_change() gives dr/ds, so multiply by s_dot - # to obtain the actual time derivative r_dot. - r_cm_dot = ( - self._get_radius_rate_of_change(shift_distance) * state.shift_velocity - ) - - phi = self._get_wrap_angle(shift_distance) - beta = SHEAVE_ANGLE / 2 - - # Runtime dynamics terms - tau_load = get_required_kwarg(kwargs, "external_load_torque") - I_s = get_required_kwarg(kwargs, "secondary_inertia") - - # Helix / spring terms - dtheta_ds = self.theta_ramp.dtheta_dx(shift_distance) - theta_total = self.initial_rotation + self.theta_ramp.theta(shift_distance) - x_total = self.initial_compression - shift_distance - - spring_term = ( - dtheta_ds * self.spring_coeff_tors * theta_total - + 2.0 * self.spring_coeff_comp * x_total - ) - - belt_mass_term = RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * r_cm * phi - μ_branch = self.μ_static if is_stick else self.μ_kinetic - - spring_numerator_term = μ_branch * np.tan(beta) * spring_term - if is_stick: - load_numerator_term = belt_mass_term * ((r_cm * tau_load) / I_s) - shift_numerator_term = -2.0 * belt_mass_term * r_cm_dot * angular_velocity - common_numerator = ( - spring_numerator_term + load_numerator_term + shift_numerator_term - ) - numerator_net = (r_eff / cvt_ratio) * common_numerator - - helix_feedback = r_eff * μ_branch * np.tan(beta) * dtheta_ds - inertial_feedback = r_eff * belt_mass_term * (r_cm / I_s) - positive_denominator = 1.0 - helix_feedback + inertial_feedback - negative_denominator = 1.0 + helix_feedback - inertial_feedback - - tau_positive = numerator_net / positive_denominator - tau_negative = -numerator_net / negative_denominator - - denominator_positive_breakdown = SecondaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - helix_feedback_term=-helix_feedback, - inertial_feedback_term=inertial_feedback, - net=positive_denominator, - ) - - denominator_negative_breakdown = SecondaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - helix_feedback_term=helix_feedback, - inertial_feedback_term=-inertial_feedback, - net=negative_denominator, - ) - else: - load_numerator_term = -belt_mass_term * ( - r_cm * ((v_b_star - state.v_b) / T_b) - ) - shift_numerator_term = -belt_mass_term * (r_cm_dot * state.v_b) - common_numerator = ( - spring_numerator_term + load_numerator_term + shift_numerator_term - ) - numerator_net = (r_eff / cvt_ratio) * common_numerator - - helix_feedback = r_eff * μ_branch * np.tan(beta) * dtheta_ds - positive_denominator = 1.0 - helix_feedback - negative_denominator = 1.0 + helix_feedback - - tau_positive = numerator_net / positive_denominator - tau_negative = -numerator_net / negative_denominator - - denominator_positive_breakdown = SecondaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - helix_feedback_term=-helix_feedback, - inertial_feedback_term=0.0, - net=positive_denominator, - ) - - denominator_negative_breakdown = SecondaryTorqueDenominatorBreakdown( - inverse_radius_term=1.0, - helix_feedback_term=helix_feedback, - inertial_feedback_term=0.0, - net=negative_denominator, - ) - - numerator_breakdown = SecondaryTorqueNumeratorBreakdown( - spring_term=(r_eff / cvt_ratio) * spring_numerator_term, - load_term=(r_eff / cvt_ratio) * load_numerator_term, - shift_term=(r_eff / cvt_ratio) * shift_numerator_term, - net=numerator_net, - ) - - return SecondaryTorqueBoundsBreakdown( - tau_negative=tau_negative, - tau_positive=tau_positive, - numerator=numerator_breakdown, - denominator_positive=denominator_positive_breakdown, - denominator_negative=denominator_negative_breakdown, - ) - - # Private helper methods - - def _calculate_helix_force( - self, torque: float, shift_distance: float - ) -> HelixForceBreakdown: - """ - Calculate helix cam force from transmitted torque. - - Uses Eq. 8.16 helix term: - F_s,helix,ax = [τ_s + k_s,0(θ_s,0 + θ_s(s)) * dθ_s/ds] / 2 - """ - shift_distance = np.clip(shift_distance, 0, MAX_SHIFT) - - spring_torque_breakdown = self._calculate_spring_tors_torque(shift_distance) - angle_multiplier = self.theta_ramp.angle_multiplier(shift_distance) - dtheta_ds = self.theta_ramp.dtheta_dx(shift_distance) - helix_angle = np.arctan2(1.0, self.helix_radius * dtheta_ds) - - net = (torque + spring_torque_breakdown.net) * dtheta_ds / 2 - - return HelixForceBreakdown( - feedbackTorque=torque, - springTorque=spring_torque_breakdown, - angle=helix_angle, - radius=self.helix_radius, - angle_multiplier=angle_multiplier, - net=net, - ) - - def _calculate_spring_comp_force( - self, shift_distance: float - ) -> springCompForceBreakdown: - """Calculate compression spring force (static clamping).""" - shift_distance = np.clip(shift_distance, 0, MAX_SHIFT) - total_compression = self.initial_compression + shift_distance - net = tm.hookes_law_comp(self.spring_coeff_comp, total_compression) - - return springCompForceBreakdown( - compression=shift_distance, - net=net, - ) - - def _calculate_spring_tors_torque( - self, shift_distance: float - ) -> SpringTorsForceBreakdown: - """Calculate torsion spring torque from preload + ramp rotation.""" - shift_distance = np.clip(shift_distance, 0, MAX_SHIFT) - - rotation_from_shift = self.theta_ramp.theta(shift_distance) - total_rotation = self.initial_rotation + rotation_from_shift - net = tm.hookes_law_tors(self.spring_coeff_tors, total_rotation) - - return SpringTorsForceBreakdown( - rotation=total_rotation, - net=net, - ) diff --git a/cvtModel/src/cvt_simulator/models/secondary_pulley_model.py b/cvtModel/src/cvt_simulator/models/secondary_pulley_model.py deleted file mode 100644 index bfaaa6cc..00000000 --- a/cvtModel/src/cvt_simulator/models/secondary_pulley_model.py +++ /dev/null @@ -1,69 +0,0 @@ -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.models.dataTypes import SecondaryPulleyDynamicsBreakdown -from cvt_simulator.models.external_load_model import LoadModel -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.constants.car_specs import ( - GEARBOX_RATIO, - WHEEL_RADIUS, - SECONDARY_INERTIA, - GEARBOX_INERTIA, - WHEEL_INERTIA, -) - - -class SecondaryPulleyModel: - """Secondary-pulley-side angular acceleration model.""" - - def __init__( - self, - car_mass: float, - load_model: LoadModel, - ): - self.car_mass = car_mass - self.load_model = load_model - # I_s: secondary-side equivalent rotational inertia used across coupled dynamics. - self.inertia = self._calculate_total_inertia() - - def get_breakdown( - self, state: SystemState, coupling_torque: float - ) -> SecondaryPulleyDynamicsBreakdown: - external_forces = self.load_model.get_breakdown(state) - - primary_to_secondary_ratio = ( - tm.current_effective_cvt_ratio(state.shift_distance) * GEARBOX_RATIO - ) - - # Linear acceleration at the vehicle from torque balance at the secondary side. - linear_accel = ( - WHEEL_RADIUS - * ( - coupling_torque * primary_to_secondary_ratio - - external_forces.net_torque_at_secondary - ) - / self.inertia - ) - - return SecondaryPulleyDynamicsBreakdown( - coupling_torque_at_secondary_pulley=( - coupling_torque * primary_to_secondary_ratio - ), - external_load_torque_at_secondary_pulley=( - external_forces.net_torque_at_secondary - ), - external_forces=external_forces, - secondary_pulley_angular_acceleration=linear_accel / WHEEL_RADIUS, - ) - - def _calculate_total_inertia(self) -> float: - """ - Calculate total system inertia using: I_s = I_sec + I_gb + (I_wheel + m*r_w^2) / G^2 - - Returns: - Total inertia in kg*m^2 - """ - wheel_and_car_inertia = WHEEL_INERTIA + self.car_mass * WHEEL_RADIUS**2 - return ( - SECONDARY_INERTIA - + GEARBOX_INERTIA - + wheel_and_car_inertia / (GEARBOX_RATIO**2) - ) diff --git a/cvtModel/src/cvt_simulator/models/slip_model.py b/cvtModel/src/cvt_simulator/models/slip_model.py deleted file mode 100644 index e67340e3..00000000 --- a/cvtModel/src/cvt_simulator/models/slip_model.py +++ /dev/null @@ -1,340 +0,0 @@ -import numpy as np -from cvt_simulator.models.dataTypes import ( - PrimaryTorqueBoundsBreakdown, - SecondaryTorqueBoundsBreakdown, - SlipBreakdown, -) -from cvt_simulator.models.external_load_model import LoadModel -from cvt_simulator.models.engine_model import EngineModel -from cvt_simulator.models.pulley.primary_pulley_interface import PrimaryPulleyModel -from cvt_simulator.models.pulley.secondary_pulley_interface import SecondaryPulleyModel -from cvt_simulator.models.primary_pulley_model import ( - PrimaryPulleyModel as PrimaryPulleyDynamicsModel, -) -from cvt_simulator.models.secondary_pulley_model import ( - SecondaryPulleyModel as SecondaryPulleyDynamicsModel, -) -from cvt_simulator.models.belt_model import BeltModel -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.utils.blending import SignalBlendController -from cvt_simulator.constants.car_specs import ( - GEARBOX_RATIO, - MAX_SHIFT, -) -from cvt_simulator.constants.tuning import ( - SLIP_CORRECTION_GAIN, - SLIP_HIGH_SPEED_LOCK_THRESHOLD, - SLIP_LOW_SPEED_BLEND_DEADZONE, - SLIP_LOW_SPEED_BLEND_TRANSITION, - SLIP_SHIFT_STOP_BLEND_DISTANCE, - SLIP_SPEED_SMOOTHING, - SLIP_TORQUE_EXIT_MARGIN, - SLIP_TORQUE_REENTER_MARGIN, -) -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm - - -class SlipModel: - def __init__( - self, - load_model: LoadModel, - engine_model: EngineModel, - car_mass: float, - primary_pulley: PrimaryPulleyModel, - secondary_pulley: SecondaryPulleyModel, - primary_pulley_model: PrimaryPulleyDynamicsModel, - secondary_pulley_model: SecondaryPulleyDynamicsModel, - belt_model: BeltModel, - ): - self.load_model = load_model - self.engine_model = engine_model - self.car_mass = car_mass - self.primary_pulley = primary_pulley - self.secondary_pulley = secondary_pulley - self.primary_pulley_model = primary_pulley_model - self.secondary_pulley_model = secondary_pulley_model - self.belt_model = belt_model - self._last_is_stick: bool | None = None - self.slip_speed_smoothing = SLIP_SPEED_SMOOTHING - self.slip_correction_gain = SLIP_CORRECTION_GAIN - self.shift_stop_blend_distance = SLIP_SHIFT_STOP_BLEND_DISTANCE - # Hysteresis around torque feasibility to avoid boundary chatter. - self.torque_exit_margin = SLIP_TORQUE_EXIT_MARGIN - self.torque_reenter_margin = SLIP_TORQUE_REENTER_MARGIN - # In slip mode, stay near demand only at very low relative speed. - # Around 0.5 km/h (0.1389 m/s) and below, blend toward no-slip demand. - # Above this region, enforce traction-bound saturation. - # TODO: Dead code, remove - self.slip_blend_controller = SignalBlendController( - deadzone=SLIP_LOW_SPEED_BLEND_DEADZONE, - transition_width=SLIP_LOW_SPEED_BLEND_TRANSITION, - ) - # High-slip engagement lock: above 5 km/h relative speed, force the - # coupling torque to the active traction bound. - self.high_slip_bound_controller = SignalBlendController( - deadzone=SLIP_HIGH_SPEED_LOCK_THRESHOLD, - transition_width=0.0, - hard_threshold=True, - ) - - def reset_mode_state(self) -> None: - self._last_is_stick = None - - def get_breakdown(self, state: SystemState) -> SlipBreakdown: - """ - Calculate slip breakdown using pulley models directly. - - Args: - state: Current system state - - Returns: - SlipBreakdown with slip analysis - """ - effective_cvt_ratio_time_derivative = ( - tm.current_effective_cvt_ratio_time_derivative( - state.shift_distance, state.shift_velocity - ) - ) - - ( - v_b_star, - T_b, - v_delta, - primary_belt_speed, - secondary_belt_speed, - ) = self.belt_model.get_kinematic_terms(state) - stick_enter_tolerance, stick_exit_tolerance = ( - self.belt_model.get_stick_speed_tolerances( - primary_belt_speed, - secondary_belt_speed, - ) - ) - - ( - tau_lower_stick, - tau_upper_stick, - primary_bounds_stick, - secondary_bounds_stick, - ) = self.calculate_coupling_torque_bounds( - state, - is_stick=True, - v_b_star=v_b_star, - T_b=T_b, - ) - - tau_ns = self.get_no_slip_torque(state) - is_stick = self._select_stick_mode( - tau_ns=tau_ns, - tau_lower=tau_lower_stick, - tau_upper=tau_upper_stick, - relative_speed=v_delta, - stick_enter_tolerance=stick_enter_tolerance, - stick_exit_tolerance=stick_exit_tolerance, - ) - - if is_stick: - tau_lower = tau_lower_stick - tau_upper = tau_upper_stick - primary_bounds = primary_bounds_stick - secondary_bounds = secondary_bounds_stick - else: - ( - tau_lower, - tau_upper, - primary_bounds, - secondary_bounds, - ) = self.calculate_coupling_torque_bounds( - state, - is_stick=False, - v_b_star=v_b_star, - T_b=T_b, - ) - - coupling_torque = self._traction_limited_coupling_torque( - v_delta=v_delta, - tau_ns=tau_ns, - tau_lower=tau_lower, - tau_upper=tau_upper, - is_stick=is_stick, - ) - - # 3) Define is_slipping for diagnostics (no effect on dynamics) - is_slipping = not is_stick - - return SlipBreakdown( - coupling_torque=coupling_torque, - torque_demand=tau_ns, - tau_upper=tau_upper, - tau_lower=tau_lower, - primary_tau_bounds=primary_bounds, - secondary_tau_bounds=secondary_bounds, - effective_cvt_ratio_time_derivative=effective_cvt_ratio_time_derivative, - is_slipping=is_slipping, - ) - - def _select_stick_mode( - self, - tau_ns: float, - tau_lower: float, - tau_upper: float, - relative_speed: float, - stick_enter_tolerance: float, - stick_exit_tolerance: float, - ) -> bool: - if tau_lower > tau_upper: - self._last_is_stick = False - return False - - rel_abs = abs(relative_speed) - - if self._last_is_stick is None: - is_stick = (tau_lower <= tau_ns <= tau_upper) and ( - rel_abs <= stick_enter_tolerance - ) - elif self._last_is_stick: - is_stick = (tau_lower - self.torque_exit_margin) <= tau_ns <= ( - tau_upper + self.torque_exit_margin - ) and (rel_abs <= stick_exit_tolerance) - else: - is_stick = (tau_lower + self.torque_reenter_margin) <= tau_ns <= ( - tau_upper - self.torque_reenter_margin - ) and (rel_abs <= stick_enter_tolerance) - - self._last_is_stick = is_stick - return is_stick - - def get_no_slip_torque(self, state: SystemState): - # Match the normalized closed-form torque-demand equation: - # tau_p = [tau_eng + (I_p/I_s) * R * tau_load - I_p * omega_s * R_dot] - # / [1 + (I_p/I_s) * R^2] - I_p = self.primary_pulley_model.inertia - I_s = self.secondary_pulley_model.inertia - - tau_eng = self.engine_model.get_torque(state.primary_pulley_angular_velocity) - tau_load = self.load_model.get_breakdown(state).net_torque_at_secondary - - R = tm.current_effective_cvt_ratio(state.shift_distance) * GEARBOX_RATIO - shift_velocity = state.shift_velocity - shift_distance = state.shift_distance - if shift_velocity > 0.0: - distance_to_max = max(MAX_SHIFT - shift_distance, 0.0) - if distance_to_max < self.shift_stop_blend_distance: - blend = distance_to_max / self.shift_stop_blend_distance - shift_velocity *= blend * blend - elif shift_velocity < 0.0: - distance_to_min = max(shift_distance, 0.0) - if distance_to_min < self.shift_stop_blend_distance: - blend = distance_to_min / self.shift_stop_blend_distance - shift_velocity *= blend * blend - - R_dot = ( - tm.current_effective_cvt_ratio_time_derivative( - shift_distance, - shift_velocity, - ) - * GEARBOX_RATIO - ) - - omega_s = state.secondary_pulley_angular_velocity - inertia_ratio = I_p / I_s - - numerator = tau_eng + inertia_ratio * R * tau_load - I_p * omega_s * R_dot - denominator = 1 + inertia_ratio * (R**2) - - return numerator / denominator - - def calculate_coupling_torque_bounds( - self, - state: SystemState, - is_stick: bool, - v_b_star: float, - T_b: float, - ) -> tuple[ - float, - float, - PrimaryTorqueBoundsBreakdown, - SecondaryTorqueBoundsBreakdown, - ]: - """ - Calculate coupling torque limits from both pulleys. - - The coupled belt contact must satisfy both primary and secondary traction - bounds. We therefore use: - - Most restrictive positive bound: min(primary_upper, secondary_upper) - - Most restrictive negative bound: max(primary_lower, secondary_lower) - - Args: - state: Current system state - - Returns: - tuple: - (coupling_tau_lower, coupling_tau_upper, - primary_tau_bounds, secondary_tau_bounds) - """ - primary_bounds = self.primary_pulley.calculate_torque_bounds( - state, - engine_drive_torque=self.engine_model.get_torque( - state.primary_pulley_angular_velocity - ), - primary_inertia=self.primary_pulley_model.inertia, - is_stick=is_stick, - v_b_star=v_b_star, - T_b=T_b, - ) - - load_torque = self.load_model.get_breakdown(state).net_torque_at_secondary - secondary_bounds = self.secondary_pulley.calculate_torque_bounds( - state, - external_load_torque=load_torque, - secondary_inertia=self.secondary_pulley_model.inertia, - is_stick=is_stick, - v_b_star=v_b_star, - T_b=T_b, - ) - - coupling_tau_lower = max( - primary_bounds.tau_lower, secondary_bounds.tau_negative - ) - coupling_tau_upper = min( - primary_bounds.tau_upper, secondary_bounds.tau_positive - ) - - return ( - coupling_tau_lower, - coupling_tau_upper, - primary_bounds, - secondary_bounds, - ) - - def _traction_limited_coupling_torque( - self, - v_delta: float, - tau_ns: float, - tau_lower: float, - tau_upper: float, - is_stick: bool, - ) -> float: - if tau_lower > tau_upper: - return 0.0 - - tau_ns_clamped = np.clip(tau_ns, tau_lower, tau_upper) - if is_stick: - # In stick, static friction enforces no-slip demand up to traction limits. - return float(tau_ns_clamped) - - tau_amp = 0.5 * (tau_upper - tau_lower) - # Slip correction is continuous at v_delta=0 and bounded by traction. - slip_correction = ( - self.slip_correction_gain - * tau_amp - * np.tanh(v_delta / self.slip_speed_smoothing) - ) - tau_low_speed = tau_ns_clamped + slip_correction - - tau_bound = tau_upper if v_delta >= 0.0 else tau_lower - tau = self.high_slip_bound_controller.blend( - low_value=tau_low_speed, - high_value=tau_bound, - signal=v_delta, - ) - return float(np.clip(tau, tau_lower, tau_upper)) diff --git a/cvtModel/src/cvt_simulator/models/system_model.py b/cvtModel/src/cvt_simulator/models/system_model.py deleted file mode 100644 index 7d9947c1..00000000 --- a/cvtModel/src/cvt_simulator/models/system_model.py +++ /dev/null @@ -1,70 +0,0 @@ -from cvt_simulator.models.dataTypes import DrivetrainBreakdown -from cvt_simulator.models.slip_model import SlipModel -from cvt_simulator.models.primary_pulley_model import PrimaryPulleyModel -from cvt_simulator.models.secondary_pulley_model import SecondaryPulleyModel -from cvt_simulator.models.cvt_shift_model import CvtShiftModel -from cvt_simulator.models.belt_model import BeltModel -from cvt_simulator.utils.system_state import SystemState - - -class SystemModel: - """Single system model that manages all component interactions and dependencies.""" - - def __init__( - self, - slip_model: SlipModel, - belt_model: BeltModel, - primary_pulley_model: PrimaryPulleyModel, - secondary_pulley_model: SecondaryPulleyModel, - cvt_shift_model: CvtShiftModel, - ): - self.slip_model = slip_model - self.belt_model = belt_model - self.primary_pulley_model = primary_pulley_model - self.secondary_pulley_model = secondary_pulley_model - self.cvt_shift_model = cvt_shift_model - - def get_breakdown(self, state: SystemState) -> DrivetrainBreakdown: - """ - Calculate the complete system breakdown in dependency order. - - Dependency order: - 1. Slip (can calculate T_max directly from pulley models) - 2. CVT Shift (needs slip for coupling_torque) - 3. Engine (needs torque through belt) - 4. Car (needs torque through belt) - """ - - # Step 1: Calculate slip dynamics (using pulley models directly) - slip_breakdown = self.slip_model.get_breakdown(state) - - # Step 2: Calculate CVT dynamics with actual coupling_torque from slip model - cvt_breakdown = self.cvt_shift_model.get_breakdown( - state, slip_breakdown.coupling_torque - ) - - # Step 3: Calculate primary-pulley-side dynamics (using slip) - primary_pulley_breakdown = self.primary_pulley_model.get_breakdown( - state, slip_breakdown.coupling_torque - ) - - # Step 4: Calculate secondary-pulley-side dynamics (using slip) - secondary_pulley_breakdown = self.secondary_pulley_model.get_breakdown( - state, slip_breakdown.coupling_torque - ) - - # Step 5: Belt transport-state evolution - belt_state_breakdown = self.belt_model.get_breakdown( - state, - primary_pulley_angular_accel=primary_pulley_breakdown.primary_pulley_angular_acceleration, - secondary_pulley_angular_accel=secondary_pulley_breakdown.secondary_pulley_angular_acceleration, - is_stick_override=not slip_breakdown.is_slipping, - ) - - return DrivetrainBreakdown( - belt_slip=slip_breakdown, - belt_state=belt_state_breakdown, - primary_pulley=primary_pulley_breakdown, - secondary_pulley=secondary_pulley_breakdown, - cvt_dynamics=cvt_breakdown, - ) diff --git a/cvtModel/src/cvt_simulator/models/ramps/__init__.py b/cvtModel/src/cvt_simulator/ramps/__init__.py similarity index 100% rename from cvtModel/src/cvt_simulator/models/ramps/__init__.py rename to cvtModel/src/cvt_simulator/ramps/__init__.py diff --git a/cvtModel/src/cvt_simulator/models/ramps/archive/cubic_spiral_zero_k1.py b/cvtModel/src/cvt_simulator/ramps/archive/cubic_spiral_zero_k1.py similarity index 98% rename from cvtModel/src/cvt_simulator/models/ramps/archive/cubic_spiral_zero_k1.py rename to cvtModel/src/cvt_simulator/ramps/archive/cubic_spiral_zero_k1.py index 2e092d48..15bd0fba 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/archive/cubic_spiral_zero_k1.py +++ b/cvtModel/src/cvt_simulator/ramps/archive/cubic_spiral_zero_k1.py @@ -1,4 +1,4 @@ -from cvt_simulator.models.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_segment import RampSegment import math from scipy.integrate import quad diff --git a/cvtModel/src/cvt_simulator/models/ramps/archive/cubic_spiral_zero_zero.py b/cvtModel/src/cvt_simulator/ramps/archive/cubic_spiral_zero_zero.py similarity index 98% rename from cvtModel/src/cvt_simulator/models/ramps/archive/cubic_spiral_zero_zero.py rename to cvtModel/src/cvt_simulator/ramps/archive/cubic_spiral_zero_zero.py index e65e90ce..97657c39 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/archive/cubic_spiral_zero_zero.py +++ b/cvtModel/src/cvt_simulator/ramps/archive/cubic_spiral_zero_zero.py @@ -1,4 +1,4 @@ -from cvt_simulator.models.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_segment import RampSegment import math from scipy.integrate import quad diff --git a/cvtModel/src/cvt_simulator/models/ramps/archive/euler_spiral_segment.py b/cvtModel/src/cvt_simulator/ramps/archive/euler_spiral_segment.py similarity index 98% rename from cvtModel/src/cvt_simulator/models/ramps/archive/euler_spiral_segment.py rename to cvtModel/src/cvt_simulator/ramps/archive/euler_spiral_segment.py index 6ca006c8..28b39bc3 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/archive/euler_spiral_segment.py +++ b/cvtModel/src/cvt_simulator/ramps/archive/euler_spiral_segment.py @@ -1,4 +1,4 @@ -from cvt_simulator.models.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_segment import RampSegment import numpy as np import math from scipy.integrate import quad diff --git a/cvtModel/src/cvt_simulator/models/ramps/archive/generate_sample_ramps.py b/cvtModel/src/cvt_simulator/ramps/archive/generate_sample_ramps.py similarity index 90% rename from cvtModel/src/cvt_simulator/models/ramps/archive/generate_sample_ramps.py rename to cvtModel/src/cvt_simulator/ramps/archive/generate_sample_ramps.py index 8b4d2f6e..bd5e694c 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/archive/generate_sample_ramps.py +++ b/cvtModel/src/cvt_simulator/ramps/archive/generate_sample_ramps.py @@ -1,12 +1,12 @@ import math from cvt_simulator.constants.car_specs import INITIAL_FLYWEIGHT_RADIUS from cvt_simulator.utils.conversions import meter_to_inch -from cvt_simulator.models.ramps.circular_segment import CircularSegment -from cvt_simulator.models.ramps.archive.cubic_spiral_zero_k1 import CubicSpiralZeroK1 -from cvt_simulator.models.ramps.linear_segment import LinearSegment -from cvt_simulator.models.ramps.piecewise_ramp import PiecewiseRamp -from cvt_simulator.models.ramps.archive.pro_defined_segment import ProDefinedSegment -from cvt_simulator.utils.ramp_utils import save_ramp_to_dxf, visualize_ramps +from cvt_simulator.ramps.circular_segment import CircularSegment +from cvt_simulator.ramps.archive.cubic_spiral_zero_k1 import CubicSpiralZeroK1 +from cvt_simulator.ramps.linear_segment import LinearSegment +from cvt_simulator.ramps.piecewise_ramp import PiecewiseRamp +from cvt_simulator.ramps.archive.pro_defined_segment import ProDefinedSegment +from cvt_simulator.test_scripts.ramp_utils import save_ramp_to_dxf, visualize_ramps ## TODO: Remove this file once we extract some of the useful ramps from it diff --git a/cvtModel/src/cvt_simulator/models/ramps/archive/pro_defined_segment.py b/cvtModel/src/cvt_simulator/ramps/archive/pro_defined_segment.py similarity index 96% rename from cvtModel/src/cvt_simulator/models/ramps/archive/pro_defined_segment.py rename to cvtModel/src/cvt_simulator/ramps/archive/pro_defined_segment.py index 247727b1..4305d05b 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/archive/pro_defined_segment.py +++ b/cvtModel/src/cvt_simulator/ramps/archive/pro_defined_segment.py @@ -1,6 +1,6 @@ import math from cvt_simulator.constants.car_specs import INITIAL_FLYWEIGHT_RADIUS -from cvt_simulator.models.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_segment import RampSegment class ProDefinedSegment(RampSegment): diff --git a/cvtModel/src/cvt_simulator/models/ramps/circular_segment.py b/cvtModel/src/cvt_simulator/ramps/circular_segment.py similarity index 99% rename from cvtModel/src/cvt_simulator/models/ramps/circular_segment.py rename to cvtModel/src/cvt_simulator/ramps/circular_segment.py index 4f2d3b80..52262c77 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/circular_segment.py +++ b/cvtModel/src/cvt_simulator/ramps/circular_segment.py @@ -1,4 +1,4 @@ -from cvt_simulator.models.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_segment import RampSegment import numpy as np import math diff --git a/cvtModel/src/cvt_simulator/models/ramps/linear_segment.py b/cvtModel/src/cvt_simulator/ramps/linear_segment.py similarity index 95% rename from cvtModel/src/cvt_simulator/models/ramps/linear_segment.py rename to cvtModel/src/cvt_simulator/ramps/linear_segment.py index a282f495..038c6830 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/linear_segment.py +++ b/cvtModel/src/cvt_simulator/ramps/linear_segment.py @@ -1,4 +1,4 @@ -from cvt_simulator.models.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_segment import RampSegment import math diff --git a/cvtModel/src/cvt_simulator/models/ramps/piecewise_ramp.py b/cvtModel/src/cvt_simulator/ramps/piecewise_ramp.py similarity index 96% rename from cvtModel/src/cvt_simulator/models/ramps/piecewise_ramp.py rename to cvtModel/src/cvt_simulator/ramps/piecewise_ramp.py index 8e22b76f..c94c960f 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/piecewise_ramp.py +++ b/cvtModel/src/cvt_simulator/ramps/piecewise_ramp.py @@ -1,9 +1,9 @@ -from cvt_simulator.models.ramps.ramp_segment import RampSegment -from cvt_simulator.models.ramps.ramp_serialization import ( +from cvt_simulator.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.ramp_serialization import ( segment_to_config, config_to_segment, ) -from cvt_simulator.models.ramps.ramp_config import PiecewiseRampConfig +from cvt_simulator.ramps.ramp_config import PiecewiseRampConfig from typing import List diff --git a/cvtModel/src/cvt_simulator/models/ramps/ramp_config.py b/cvtModel/src/cvt_simulator/ramps/ramp_config.py similarity index 100% rename from cvtModel/src/cvt_simulator/models/ramps/ramp_config.py rename to cvtModel/src/cvt_simulator/ramps/ramp_config.py diff --git a/cvtModel/src/cvt_simulator/models/ramps/ramp_preview.py b/cvtModel/src/cvt_simulator/ramps/ramp_preview.py similarity index 96% rename from cvtModel/src/cvt_simulator/models/ramps/ramp_preview.py rename to cvtModel/src/cvt_simulator/ramps/ramp_preview.py index 03ae42be..99964da5 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/ramp_preview.py +++ b/cvtModel/src/cvt_simulator/ramps/ramp_preview.py @@ -5,8 +5,8 @@ from typing import List, Dict, Union import numpy as np -from cvt_simulator.models.ramps.piecewise_ramp import PiecewiseRamp -from cvt_simulator.models.ramps.ramp_config import PiecewiseRampConfig +from cvt_simulator.ramps.piecewise_ramp import PiecewiseRamp +from cvt_simulator.ramps.ramp_config import PiecewiseRampConfig from cvt_simulator.utils.conversions import inch_to_meter from cvt_simulator.constants.car_specs import MAX_SHIFT diff --git a/cvtModel/src/cvt_simulator/models/ramps/ramp_segment.py b/cvtModel/src/cvt_simulator/ramps/ramp_segment.py similarity index 100% rename from cvtModel/src/cvt_simulator/models/ramps/ramp_segment.py rename to cvtModel/src/cvt_simulator/ramps/ramp_segment.py diff --git a/cvtModel/src/cvt_simulator/models/ramps/ramp_serialization.py b/cvtModel/src/cvt_simulator/ramps/ramp_serialization.py similarity index 91% rename from cvtModel/src/cvt_simulator/models/ramps/ramp_serialization.py rename to cvtModel/src/cvt_simulator/ramps/ramp_serialization.py index e9c36d1b..672d3b99 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/ramp_serialization.py +++ b/cvtModel/src/cvt_simulator/ramps/ramp_serialization.py @@ -6,10 +6,10 @@ """ from typing import Type, Callable -from cvt_simulator.models.ramps.ramp_segment import RampSegment -from cvt_simulator.models.ramps.linear_segment import LinearSegment -from cvt_simulator.models.ramps.circular_segment import CircularSegment -from cvt_simulator.models.ramps.ramp_config import ( +from cvt_simulator.ramps.ramp_segment import RampSegment +from cvt_simulator.ramps.linear_segment import LinearSegment +from cvt_simulator.ramps.circular_segment import CircularSegment +from cvt_simulator.ramps.ramp_config import ( LinearSegmentConfig, CircularSegmentConfig, RampSegmentConfig, diff --git a/cvtModel/src/cvt_simulator/models/ramps/theta_ramp.py b/cvtModel/src/cvt_simulator/ramps/theta_ramp.py similarity index 96% rename from cvtModel/src/cvt_simulator/models/ramps/theta_ramp.py rename to cvtModel/src/cvt_simulator/ramps/theta_ramp.py index 6e8f45e4..d1d150e9 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/theta_ramp.py +++ b/cvtModel/src/cvt_simulator/ramps/theta_ramp.py @@ -16,9 +16,9 @@ """ import math -from cvt_simulator.models.ramps.piecewise_ramp import PiecewiseRamp -from cvt_simulator.models.ramps.linear_segment import LinearSegment -from cvt_simulator.models.ramps.circular_segment import CircularSegment +from cvt_simulator.ramps.piecewise_ramp import PiecewiseRamp +from cvt_simulator.ramps.linear_segment import LinearSegment +from cvt_simulator.ramps.circular_segment import CircularSegment class ThetaRamp: diff --git a/cvtModel/src/cvt_simulator/models/ramps/theta_ramp_visualization.py b/cvtModel/src/cvt_simulator/ramps/theta_ramp_visualization.py similarity index 98% rename from cvtModel/src/cvt_simulator/models/ramps/theta_ramp_visualization.py rename to cvtModel/src/cvt_simulator/ramps/theta_ramp_visualization.py index 184697d3..bb083c40 100644 --- a/cvtModel/src/cvt_simulator/models/ramps/theta_ramp_visualization.py +++ b/cvtModel/src/cvt_simulator/ramps/theta_ramp_visualization.py @@ -11,8 +11,8 @@ import numpy as np import matplotlib.pyplot as plt -from cvt_simulator.models.ramps import CircularSegment, LinearSegment, PiecewiseRamp -from cvt_simulator.models.ramps.theta_ramp import ThetaRamp +from cvt_simulator.ramps import CircularSegment, LinearSegment, PiecewiseRamp +from cvt_simulator.ramps.theta_ramp import ThetaRamp from cvt_simulator.constants.car_specs import HELIX_RADIUS, MAX_SHIFT diff --git a/cvtModel/src/cvt_simulator/utils/simulation_args.py b/cvtModel/src/cvt_simulator/sim_utils/simulation_args.py similarity index 75% rename from cvtModel/src/cvt_simulator/utils/simulation_args.py rename to cvtModel/src/cvt_simulator/sim_utils/simulation_args.py index d6209009..565a2c84 100644 --- a/cvtModel/src/cvt_simulator/utils/simulation_args.py +++ b/cvtModel/src/cvt_simulator/sim_utils/simulation_args.py @@ -1,22 +1,37 @@ from dataclasses import dataclass, fields, replace, field, is_dataclass from typing import Any, Mapping, Union, get_args, get_origin -from cvt_simulator.models.ramps.ramp_config import PiecewiseRampConfig -from cvt_simulator.models.pulley.primary_pulley_flyweight import ( - create_default_flyweight_ramp, -) -from cvt_simulator.models.pulley.secondary_pulley_torque_reactive import ( - create_default_helix_ramp, +from cvt_simulator.ramps.ramp_config import ( + PiecewiseRampConfig, + LinearSegmentConfig, + CircularSegmentConfig, ) +from cvt_simulator.utils.conversions import inch_to_meter +from cvt_simulator.constants.car_specs import MAX_SHIFT def _get_default_primary_ramp() -> PiecewiseRampConfig: """Factory function for default primary ramp config.""" - return create_default_flyweight_ramp().to_config() + # Default flyweight ramp: small linear engagement then circular arc finish + return PiecewiseRampConfig( + segments=[ + LinearSegmentConfig(length=inch_to_meter(0.125), angle=25.0), + CircularSegmentConfig( + length=inch_to_meter(1.0), + angle_start=33.4248111826, + angle_end=20.8067910127, + quadrant=2, + ), + ] + ) def _get_default_secondary_ramp() -> PiecewiseRampConfig: """Factory function for default secondary ramp config.""" - return create_default_helix_ramp().angle_ramp.to_config() + # Default helix angle ramp: constant helix angle across full shift + helix_angle_deg = 36.0 + return PiecewiseRampConfig( + segments=[LinearSegmentConfig(length=MAX_SHIFT, angle=helix_angle_deg)] + ) @dataclass(slots=True) diff --git a/cvtModel/src/cvt_simulator/sim_utils/simulation_constraints.py b/cvtModel/src/cvt_simulator/sim_utils/simulation_constraints.py new file mode 100644 index 00000000..10704296 --- /dev/null +++ b/cvtModel/src/cvt_simulator/sim_utils/simulation_constraints.py @@ -0,0 +1,210 @@ +from cvt_simulator.constants.car_specs import ( + MAX_SHIFT, +) +from cvt_simulator.core.dynamics.contact_dynamics_model import ContactDynamicsModel +from cvt_simulator.utils.state_computations import ( + secondary_pulley_angular_velocity_to_car_velocity, +) +from cvt_simulator.sim_utils.system_state import SystemState + +MIN_CAR_VELOCITY_MPS = -20.0 + + +def update_y(y, state: SystemState): + stateArray = state.to_array() + for i in range(len(y)): + y[i] = stateArray[i] + + +def shift_constraint_event(t, y): + state = SystemState.from_array(y) + shift_velocity = state.s_dot + shift_distance = state.s + + if shift_distance < 0: + state.s = 0 + state.s_dot = max(0, shift_velocity) + + elif shift_distance > MAX_SHIFT: + state.s = MAX_SHIFT + state.s_dot = min(0, shift_velocity) + + update_y(y, state) + return 1 + + +def car_velocity_constraint_event(t, y): + state = SystemState.from_array(y) + return ( + secondary_pulley_angular_velocity_to_car_velocity( + state.ω_s + ) + - MIN_CAR_VELOCITY_MPS + ) + + +car_velocity_constraint_event.terminal = True +car_velocity_constraint_event.direction = -1 + + +def get_shift_steady_event(contact_model: ContactDynamicsModel): + """ + Returns an event function that triggers only when: + 1. The system is close enough to full shift (i.e. shift_distance within tol of MAX_SHIFT). + 2. The desired shift acceleration (as computed by shift_simulator) + transitions from negative to positive (i.e. it wants to push further). + """ + + def shift_steady_event(t, y): + state = SystemState.from_array(y) + tol = 1e-5 # Tolerance for proximity to MAX_SHIFT + + # Before we get near full shift, return a fixed negative value. + if state.s < MAX_SHIFT - tol: + return -tol + + # Clamp here as clamping from other events doesn't propagate immediately + shift_velocity = state.s_dot + shift_distance = state.s + if shift_distance < 0: + state.s = 0 + state.s_dot = max(0, shift_velocity) + + elif shift_distance > MAX_SHIFT: + state.s = MAX_SHIFT + state.s_dot = min(0, shift_velocity) + + update_y(y, state) + + # Once near full shift, return the computed shift acceleration. + # The event will trigger when this value crosses from negative to positive. + # TODO: Clean this up! + return contact_model.get_breakdown(state).shift.acceleration + + shift_steady_event.terminal = True + shift_steady_event.direction = 1 # Looking for a negative-to-positive crossing. + return shift_steady_event + + +def get_back_shift_event(contact_model: ContactDynamicsModel): + """ + Returns an event function that triggers when the system wants to back-shift + from full shift position. This detects when the shift acceleration becomes + sufficiently negative while at MAX_SHIFT. + """ + + def back_shift_event(t, y): + state = SystemState.from_array(y) + + # Should only trigger when at full shift + if state.s < MAX_SHIFT - 1e-5: + return 1.0 # Return positive value when not at full shift + + # Calculate the shift acceleration + shift_accel = contact_model.get_breakdown(state).shift.acceleration + + # Return the acceleration + small threshold + # Event triggers when this crosses from positive to negative + # (i.e., when acceleration becomes sufficiently negative) + return shift_accel + 5.0 # 5 N threshold to avoid numerical noise + + back_shift_event.terminal = True + back_shift_event.direction = -1 # Looking for a positive-to-negative crossing + return back_shift_event + + +def get_mid_shift_steady_event( + contact_model: ContactDynamicsModel, + velocity_tol: float = 1e-4, + accel_tol: float = 0.1, + wake_accel_guard_tol: float = 0.5, + boundary_margin: float = 1e-5, +): + """ + Trigger when the system is quasi-static in the shift DOF away from hard limits. + + Conditions to enter steady mode: + 1. Shift distance is not clamped against hard bounds. + 2. |shift_velocity| <= velocity_tol. + 3. |shift_accel| <= accel_tol. + 4. If shift were locked now (shift_velocity=0), the resulting + |shift_accel| must also be <= wake_accel_guard_tol. + + This is used to enter a locked mid-shift mode to avoid costly dithering. + """ + + def mid_shift_steady_event(t, y): + state = SystemState.from_array(y) + + # Only apply in the interior region, not near hard shift boundaries. + if ( + state.s <= boundary_margin + or state.s >= MAX_SHIFT - boundary_margin + ): + return 1.0 + + shift_breakdown = contact_model.get_breakdown(state).shift + shift_accel = shift_breakdown.acceleration + + # Guard against immediate wake chatter: only lock if the locked-state + # acceleration would also remain below the wake threshold. + locked_state = SystemState( + s=state.s, + s_dot=0.0, + ω_p=state.ω_p, + ω_s=state.ω_s, + ) + locked_shift_accel = contact_model.get_breakdown(locked_state).shift.acceleration + + # Deterministic event value: <= 0 means quasi-static and eligible to lock. + return max( + abs(state.s_dot) - velocity_tol, + abs(shift_accel) - accel_tol, + abs(locked_shift_accel) - wake_accel_guard_tol, + ) + + mid_shift_steady_event.terminal = True + mid_shift_steady_event.direction = ( + -1 + ) # enter steady mode when value drops below zero + return mid_shift_steady_event + + +def get_mid_shift_wake_event( + contact_model: ContactDynamicsModel, + wake_accel_tol: float = 1.5, + boundary_margin: float = 1e-5, +): + """ + Trigger when locked mid-shift mode should resume normal shift dynamics. + + Event value is negative while near equilibrium and becomes positive when + acceleration indicates the locked shift should move again. + + Direction logic: + - Near lower bound: only positive acceleration can move the shift, so wake on + shift_accel - wake_accel_tol. + - Near upper bound: only negative acceleration can move the shift, so wake on + -shift_accel - wake_accel_tol. + - Interior: wake on |shift_accel| - wake_accel_tol. + """ + + def mid_shift_wake_event(t, y): + state = SystemState.from_array(y) + shift_accel = contact_model.get_breakdown(state).shift.acceleration + if state.s <= boundary_margin: + return shift_accel - wake_accel_tol + if state.s >= MAX_SHIFT - boundary_margin: + return -shift_accel - wake_accel_tol + return abs(shift_accel) - wake_accel_tol + + mid_shift_wake_event.terminal = True + mid_shift_wake_event.direction = 1 # wake when value rises through zero + return mid_shift_wake_event + + +# Export all constraints +constraints = [ + shift_constraint_event, + car_velocity_constraint_event, +] diff --git a/cvtModel/src/cvt_simulator/utils/simulation_result.py b/cvtModel/src/cvt_simulator/sim_utils/simulation_result.py similarity index 77% rename from cvtModel/src/cvt_simulator/utils/simulation_result.py rename to cvtModel/src/cvt_simulator/sim_utils/simulation_result.py index 0f426b88..a013acc3 100644 --- a/cvtModel/src/cvt_simulator/utils/simulation_result.py +++ b/cvtModel/src/cvt_simulator/sim_utils/simulation_result.py @@ -1,4 +1,4 @@ -from cvt_simulator.utils.system_state import SystemState +from cvt_simulator.sim_utils.system_state import SystemState from typing import Any from cvt_simulator.utils.state_computations import ( integrate_positions_trapezoidal, @@ -41,10 +41,10 @@ def from_csv(filename="simulation_output.csv"): time = df["time"].values states = [ SystemState( - shift_distance=row["shift_distance"], - shift_velocity=row["shift_velocity"], - primary_pulley_angular_velocity=row["primary_pulley_angular_velocity"], - secondary_pulley_angular_velocity=row[ + s=row["shift_distance"], + s_dot=row["shift_velocity"], + ω_p=row["primary_pulley_angular_velocity"], + ω_s=row[ "secondary_pulley_angular_velocity" ], ) @@ -63,7 +63,7 @@ def write_csv(self, filename="simulation_output.csv"): self.time, [ secondary_pulley_angular_velocity_to_car_velocity( - s.secondary_pulley_angular_velocity + s.ω_s ) for s in self.states ], @@ -72,7 +72,7 @@ def write_csv(self, filename="simulation_output.csv"): self.time, [ primary_pulley_angular_velocity_to_engine_angular_velocity( - s.primary_pulley_angular_velocity + s.ω_p ) for s in self.states ], @@ -80,23 +80,23 @@ def write_csv(self, filename="simulation_output.csv"): data = { "time": self.time, - "shift_distance": [state.shift_distance for state in self.states], - "shift_velocity": [state.shift_velocity for state in self.states], + "shift_distance": [state.s for state in self.states], + "shift_velocity": [state.s_dot for state in self.states], "primary_pulley_angular_velocity": [ - state.primary_pulley_angular_velocity for state in self.states + state.ω_p for state in self.states ], "secondary_pulley_angular_velocity": [ - state.secondary_pulley_angular_velocity for state in self.states + state.ω_s for state in self.states ], "car_velocity": [ secondary_pulley_angular_velocity_to_car_velocity( - s.secondary_pulley_angular_velocity + s.ω_s ) for s in self.states ], "engine_angular_velocity": [ primary_pulley_angular_velocity_to_engine_angular_velocity( - s.primary_pulley_angular_velocity + s.ω_p ) for s in self.states ], @@ -114,23 +114,23 @@ def plot(self, field="secondary_pulley_angular_velocity"): """ # Mapping field names to their respective data field_data = { - "shift_distance": [state.shift_distance for state in self.states], - "shift_velocity": [state.shift_velocity for state in self.states], + "shift_distance": [state.s for state in self.states], + "shift_velocity": [state.s_dot for state in self.states], "primary_pulley_angular_velocity": [ - state.primary_pulley_angular_velocity for state in self.states + state.ω_p for state in self.states ], "secondary_pulley_angular_velocity": [ - state.secondary_pulley_angular_velocity for state in self.states + state.ω_s for state in self.states ], "car_velocity": [ secondary_pulley_angular_velocity_to_car_velocity( - s.secondary_pulley_angular_velocity + s.ω_s ) for s in self.states ], "engine_angular_velocity": [ primary_pulley_angular_velocity_to_engine_angular_velocity( - s.primary_pulley_angular_velocity + s.ω_p ) for s in self.states ], diff --git a/cvtModel/src/cvt_simulator/utils/system_state.py b/cvtModel/src/cvt_simulator/sim_utils/system_state.py similarity index 60% rename from cvtModel/src/cvt_simulator/utils/system_state.py rename to cvtModel/src/cvt_simulator/sim_utils/system_state.py index 7d6fee17..4f991e08 100644 --- a/cvtModel/src/cvt_simulator/utils/system_state.py +++ b/cvtModel/src/cvt_simulator/sim_utils/system_state.py @@ -12,44 +12,44 @@ class SystemState: - shift_velocity: ṡ [m/s] - rate of sheave axial movement - primary_pulley_angular_velocity: ω_P [rad/s] - primary pulley angular velocity - secondary_pulley_angular_velocity: ω_s [rad/s] - secondary pulley angular velocity - - v_b: belt linear transport speed used by slip branch dynamics [m/s] + - belt velocity: v_b [m/s] - belt linear transport speed All other quantities (car_velocity, engine_angular_velocity, positions, etc.) are derived from these DOF and should be computed using StateComputations utility. """ - shift_distance: float = 0.0 - shift_velocity: float = 0.0 - primary_pulley_angular_velocity: float = 0.0 - secondary_pulley_angular_velocity: float = 0.0 + s: float = 0.0 + s_dot: float = 0.0 + ω_p: float = 0.0 + ω_s: float = 0.0 v_b: float = 0.0 def to_array(self): """Converts the state to an array for solve_ivp.""" return [ - self.shift_distance, - self.shift_velocity, - self.primary_pulley_angular_velocity, - self.secondary_pulley_angular_velocity, + self.s, + self.s_dot, + self.ω_p, + self.ω_s, self.v_b, ] @staticmethod def from_array(array): """Creates a SystemState from an array.""" - shift_distance = float(array[0]) + s = float(array[0]) # Clamp numerical drift so downstream geometry always sees a valid shift domain. - if shift_distance < 0.0: - shift_distance = 0.0 - elif shift_distance > MAX_SHIFT: - shift_distance = float(MAX_SHIFT) + if s < 0.0: + s = 0.0 + elif s > MAX_SHIFT: + s = float(MAX_SHIFT) v_b = float(array[4]) if len(array) > 4 else 0.0 return SystemState( - shift_distance=shift_distance, - shift_velocity=array[1], - primary_pulley_angular_velocity=array[2], - secondary_pulley_angular_velocity=array[3], + s=s, + s_dot=array[1], + ω_p=array[2], + ω_s=array[3], v_b=v_b, ) diff --git a/cvtModel/src/cvt_simulator/simulation_runner.py b/cvtModel/src/cvt_simulator/simulation_runner.py index 7a5268cd..c0559cd7 100644 --- a/cvtModel/src/cvt_simulator/simulation_runner.py +++ b/cvtModel/src/cvt_simulator/simulation_runner.py @@ -2,16 +2,26 @@ import numpy as np from typing import Callable, Optional, Any from scipy.integrate import solve_ivp -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.utils.simulation_result import SimulationResult -from cvt_simulator.models.system_model import SystemModel +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.sim_utils.simulation_result import SimulationResult +from cvt_simulator.core.dynamics.contact_dynamics_model import ContactDynamicsModel from cvt_simulator.constants.car_specs import ( GEARBOX_RATIO, MAX_SHIFT, + ENGINE_INERTIA, + SECONDARY_INERTIA, + HELIX_RADIUS, ) -from cvt_simulator.utils.conversions import rpm_to_rad_s -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm -from cvt_simulator.utils.simulation_constraints import ( +from cvt_simulator.core.components.engine import EngineModel +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.core.components.vehicle_load import LoadModel +from cvt_simulator.ramps.piecewise_ramp import PiecewiseRamp +from cvt_simulator.ramps.theta_ramp import ThetaRamp +from cvt_simulator.utils.conversions import rpm_to_rad_s, deg_to_rad +from cvt_simulator.constants.engine_specs import safe_torque_curve +from cvt_simulator.sim_utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_constraints import ( car_velocity_constraint_event, get_shift_steady_event, get_back_shift_event, @@ -19,6 +29,7 @@ get_mid_shift_wake_event, shift_constraint_event, ) +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY # Helper class to wrap data @@ -36,30 +47,74 @@ class SimulationRunner: MID_SHIFT_MIN_HOLD_TIME = 0.02 # seconds MID_SHIFT_RELOCK_DELAY = 0.05 # seconds INITIAL_STATE = SystemState( - shift_distance=0.0, - shift_velocity=0.0, + s=0.0, + s_dot=0.0, # Initial secondary pulley angular velocity derived from initial car velocity - secondary_pulley_angular_velocity=rpm_to_rad_s(0.1) - / (GEARBOX_RATIO * tm.current_effective_cvt_ratio(0)), + ω_s=rpm_to_rad_s(0.1) + / (GEARBOX_RATIO * CVT_GEOMETRY.effective_cvt_ratio(0)), # Initial primary pulley angular velocity (engine speed) - primary_pulley_angular_velocity=rpm_to_rad_s(1800), + ω_p=rpm_to_rad_s(1800), v_b=0.0, ) def __init__( self, - system_model: SystemModel, + contact_model: ContactDynamicsModel, # Optional progress callback. Preferred signature: # callback(progress_percent, sim_time_s, shift_distance) # Backward-compatible signature callback(progress_percent) is also supported. progress_callback: Optional[Callable[..., None]] = None, transition_callback: Optional[Callable[[dict[str, Any]], None]] = None, ): - self.system_model = system_model + self.contact_model = contact_model self.progress_callback = progress_callback self.transition_callback = transition_callback self._last_callback_percent = -1.0 + @classmethod + def from_simulation_args( + cls, + args: SimulationArgs, + progress_callback: Optional[Callable[..., None]] = None, + transition_callback: Optional[Callable[[dict[str, Any]], None]] = None, + ) -> "SimulationRunner": + primary_ramp = PiecewiseRamp.from_config(args.primary_ramp_config) + secondary_ramp = PiecewiseRamp.from_config(args.secondary_ramp_config) + + primary_pulley = PrimaryPulley( + spring_coeff_comp=args.primary_spring_rate, + initial_compression=args.primary_spring_pretension, + flyweight_mass=args.flyweight_mass, + ramp=primary_ramp, + ) + secondary_pulley = SecondaryPulley( + spring_coeff_tors=args.secondary_torsion_spring_rate, + spring_coeff_comp=args.secondary_compression_spring_rate, + initial_rotation=deg_to_rad(args.secondary_rotational_spring_pretension), + initial_compression=args.secondary_linear_spring_pretension, + helix_ramp=ThetaRamp(secondary_ramp, HELIX_RADIUS), + helix_radius=HELIX_RADIUS, + ) + engine_model = EngineModel(safe_torque_curve) + load_model = LoadModel( + car_mass=args.vehicle_weight + args.driver_weight, + incline_angle=deg_to_rad(args.angle_of_incline), + ) + contact_model = ContactDynamicsModel( + primary_pulley=primary_pulley, + secondary_pulley=secondary_pulley, + primary_inertia=ENGINE_INERTIA, + secondary_inertia=SECONDARY_INERTIA, + belt_mass=ContactDynamicsModel.compute_belt_mass(), + engine_model=engine_model, + load_model=load_model, + ) + return cls( + contact_model=contact_model, + progress_callback=progress_callback, + transition_callback=transition_callback, + ) + def _emit_transition( self, from_mode: str, @@ -77,15 +132,13 @@ def _emit_transition( "to_mode": to_mode, "time": float(t), "reason": reason, - "shift_distance": float(state.shift_distance), - "shift_velocity": float(state.shift_velocity), + "shift_distance": float(state.s), + "shift_velocity": float(state.s_dot), } ) def run_simulation(self) -> SimulationResult: """Run the simulation and return results.""" - self.system_model.belt_model.reset_mode_state() - self.system_model.slip_model.reset_mode_state() cvt_system_ode = self._get_ode_function() # Use a single global time grid for the entire simulation time_eval = np.linspace(0, self.TOTAL_SIM_TIME, 10000) @@ -96,11 +149,6 @@ def run_simulation(self) -> SimulationResult: current_time = 0 current_state = self.INITIAL_STATE.to_array() - initial_state = SystemState.from_array(current_state) - v_b_star, _, _, _, _ = self.system_model.belt_model.get_kinematic_terms( - initial_state - ) - current_state[4] = v_b_star mode = "normal" locked_shift_distance = None @@ -153,7 +201,7 @@ def append_solution_segment(solution): if mode == "normal": base_mid_shift_steady_event = get_mid_shift_steady_event( - self.system_model + self.contact_model ) def guarded_mid_shift_steady_event(t, y): @@ -167,7 +215,7 @@ def guarded_mid_shift_steady_event(t, y): guarded_mid_shift_steady_event.direction = -1 events = [ - get_shift_steady_event(self.system_model), + get_shift_steady_event(self.contact_model), guarded_mid_shift_steady_event, car_velocity_constraint_event, shift_constraint_event, @@ -233,7 +281,7 @@ def guarded_mid_shift_steady_event(t, y): if mode == "full_shift": locked_ode = self._get_locked_shift_ode_function(MAX_SHIFT) events = [ - get_back_shift_event(self.system_model), + get_back_shift_event(self.contact_model), car_velocity_constraint_event, ] event_names = [ @@ -280,7 +328,7 @@ def guarded_mid_shift_steady_event(t, y): locked_ode = self._get_locked_shift_ode_function(locked_shift_distance) - base_mid_shift_wake_event = get_mid_shift_wake_event(self.system_model) + base_mid_shift_wake_event = get_mid_shift_wake_event(self.contact_model) def guarded_mid_shift_wake_event(t, y): # Once we lock into mid-shift, keep that mode for a minimum @@ -374,13 +422,13 @@ def guarded_mid_shift_wake_event(t, y): termination_context.setdefault("details", {}) termination_context["details"].update( { - "final_shift_distance": float(final_state.shift_distance), - "final_shift_velocity": float(final_state.shift_velocity), + "final_shift_distance": float(final_state.s), + "final_shift_velocity": float(final_state.s_dot), "final_primary_pulley_angular_velocity": float( - final_state.primary_pulley_angular_velocity + final_state.ω_p ), "final_secondary_pulley_angular_velocity": float( - final_state.secondary_pulley_angular_velocity + final_state.ω_s ), } ) @@ -535,8 +583,8 @@ def _evaluate_cvt_system(self, t: float, y: list[float]): # Do not mutate the solver state in normal mode. Use a constrained copy # for geometry/force evaluation while preserving continuous integration. - raw_shift_distance = state.shift_distance - raw_shift_velocity = state.shift_velocity + raw_shift_distance = state.s + raw_shift_velocity = state.s_dot eval_shift_distance = float(np.clip(raw_shift_distance, 0.0, MAX_SHIFT)) eval_shift_velocity = raw_shift_velocity if raw_shift_distance <= 0.0 and raw_shift_velocity < 0.0: @@ -545,25 +593,19 @@ def _evaluate_cvt_system(self, t: float, y: list[float]): eval_shift_velocity = 0.0 eval_state = SystemState( - shift_distance=eval_shift_distance, - shift_velocity=eval_shift_velocity, - primary_pulley_angular_velocity=state.primary_pulley_angular_velocity, - secondary_pulley_angular_velocity=state.secondary_pulley_angular_velocity, + s=eval_shift_distance, + s_dot=eval_shift_velocity, + ω_p=state.ω_p, + ω_s=state.ω_s, v_b=state.v_b, ) - self._print_progress(t, eval_state.shift_distance) + self._print_progress(t, eval_state.s) # Get system breakdown (this calculates everything in correct order) - drivetrain_breakdown = self.system_model.get_breakdown(eval_state) + contact_breakdown = self.contact_model.get_breakdown(eval_state) - # Extract accelerations - secondary_pulley_angular_accel_from_torques = ( - drivetrain_breakdown.secondary_pulley.secondary_pulley_angular_acceleration - ) - primary_pulley_angular_accel = ( - drivetrain_breakdown.primary_pulley.primary_pulley_angular_acceleration - ) - shift_acceleration = drivetrain_breakdown.cvt_dynamics.acceleration + # Extract acceleration + shift_acceleration = contact_breakdown.shift.acceleration # Prevent acceleration from pushing past boundaries (metal hitting metal) if eval_shift_distance <= 0 and shift_acceleration < 0: @@ -571,14 +613,12 @@ def _evaluate_cvt_system(self, t: float, y: list[float]): elif eval_shift_distance >= MAX_SHIFT and shift_acceleration > 0: shift_acceleration = 0 - v_b_dot = drivetrain_breakdown.belt_state.v_b_dot - return [ eval_shift_velocity, shift_acceleration, - primary_pulley_angular_accel, - secondary_pulley_angular_accel_from_torques, - v_b_dot, + contact_breakdown.drivetrain.ω_p_dot, + contact_breakdown.drivetrain.ω_s_dot, + contact_breakdown.drivetrain.v_b_dot, ] def _evaluate_full_shift_system(self, t: float, y: list[float]): @@ -590,8 +630,8 @@ def _evaluate_full_shift_system(self, t: float, y: list[float]): state = SystemState.from_array(y) self._print_progress(t, MAX_SHIFT) # Force the shifting variables to remain constant at full shift. - state.shift_distance = MAX_SHIFT - state.shift_velocity = 0 + state.s = MAX_SHIFT + state.s_dot = 0 # CRITICAL: Update the actual y array that scipy saves constrained_y = state.to_array() @@ -599,21 +639,14 @@ def _evaluate_full_shift_system(self, t: float, y: list[float]): y[i] = constrained_y[i] # Get system breakdown for full shift case - drivetrain_breakdown = self.system_model.get_breakdown(state) - - secondary_pulley_angular_accel_from_torques = ( - drivetrain_breakdown.secondary_pulley.secondary_pulley_angular_acceleration - ) - primary_pulley_angular_accel = ( - drivetrain_breakdown.primary_pulley.primary_pulley_angular_acceleration - ) + contact_breakdown = self.contact_model.get_breakdown(state) return [ 0, # shift_distance held constant 0, # shift_velocity held constant - primary_pulley_angular_accel, # primary pulley continues to evolve - secondary_pulley_angular_accel_from_torques, # secondary pulley continues to evolve - drivetrain_breakdown.belt_state.v_b_dot, + contact_breakdown.drivetrain.ω_p_dot, # primary pulley continues to evolve + contact_breakdown.drivetrain.ω_s_dot, # secondary pulley continues to evolve + contact_breakdown.drivetrain.v_b_dot, ] def _evaluate_locked_shift_system( @@ -626,25 +659,19 @@ def _evaluate_locked_shift_system( state = SystemState.from_array(y) self._print_progress(t, locked_shift_distance) - state.shift_distance = locked_shift_distance - state.shift_velocity = 0 + state.s = locked_shift_distance + state.s_dot = 0 constrained_y = state.to_array() for i in range(len(y)): y[i] = constrained_y[i] - drivetrain_breakdown = self.system_model.get_breakdown(state) - secondary_pulley_angular_accel_from_torques = ( - drivetrain_breakdown.secondary_pulley.secondary_pulley_angular_acceleration - ) - primary_pulley_angular_accel = ( - drivetrain_breakdown.primary_pulley.primary_pulley_angular_acceleration - ) + contact_breakdown = self.contact_model.get_breakdown(state) return [ 0, 0, - primary_pulley_angular_accel, - secondary_pulley_angular_accel_from_torques, - drivetrain_breakdown.belt_state.v_b_dot, + contact_breakdown.drivetrain.ω_p_dot, + contact_breakdown.drivetrain.ω_s_dot, + contact_breakdown.drivetrain.v_b_dot, ] diff --git a/cvtModel/src/cvt_simulator/slip/branch_algebra.py b/cvtModel/src/cvt_simulator/slip/branch_algebra.py new file mode 100644 index 00000000..3db2d8e8 --- /dev/null +++ b/cvtModel/src/cvt_simulator/slip/branch_algebra.py @@ -0,0 +1,182 @@ +"""Branch-specific algebra moved out of BranchResolver. + +Each function computes the branch kinematics and returns +tau_p, tau_s, v_b_dot for the branch. +""" +import math + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, SHEAVE_ANGLE +from cvt_simulator.constants.constants import RUBBER_ALUMINUM_KINETIC_FRICTION, RUBBER_DENSITY +from cvt_simulator.core.data_types import SlipMetricsResult +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY + + +def primary_slip_algebra( + decision: SlipMetricsResult, + state: SystemState, + tau_load: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, +) -> tuple[float, float]: + s = state.s + s_dot = state.s_dot + v_b = state.v_b + + r_p = decision.no_slip.breakdown.r_p + r_s = decision.no_slip.breakdown.r_s + r_s_dot = decision.no_slip.breakdown.r_s_dot + + r_p_cm = CVT_GEOMETRY.primary_centroid_radius(s) + r_p_cm_dot = CVT_GEOMETRY.primary_outer_radius_time_derivative(s, s_dot) + phi_p = CVT_GEOMETRY.primary_wrap_angle(s) + + F_p_ax = primary_pulley.calculate_axial_clamping_force(state).pulley_breakdown.net + + mu_k = RUBBER_ALUMINUM_KINETIC_FRICTION + beta = SHEAVE_ANGLE / 2.0 + rho_b = RUBBER_DENSITY + A_b = BELT_CROSS_SECTIONAL_AREA + + sigma_p = decision.primary_slip_direction + + numerator = ( + sigma_p * (2.0 * mu_k * F_p_ax * math.tan(beta) - rho_b * A_b * phi_p * r_p_cm_dot * v_b) + - tau_load / r_s + + I_s * r_s_dot * state.ω_s / (r_s ** 2) + ) + denominator = m_b + sigma_p * rho_b * A_b * phi_p * r_p_cm + I_s / (r_s ** 2) + v_b_dot = numerator / denominator + + tau_p = sigma_p * r_p * ( + 2.0 * mu_k * F_p_ax * math.tan(beta) + - rho_b * A_b * phi_p * (r_p_cm * v_b_dot + r_p_cm_dot * v_b) + ) + tau_s = tau_load + I_s * (v_b_dot - r_s_dot * state.ω_s) / r_s + + return tau_p, tau_s + + +def secondary_slip_algebra( + decision: SlipMetricsResult, + state: SystemState, + tau_engine: float, + I_p: float, + m_b: float, + secondary_pulley: SecondaryPulley, +) -> tuple[float, float]: + s = state.s + s_dot = state.s_dot + v_b = state.v_b + + r_p = decision.no_slip.breakdown.r_p + r_p_dot = decision.no_slip.breakdown.r_p_dot + r_s = decision.no_slip.breakdown.r_s + r_s_dot = decision.no_slip.breakdown.r_s_dot + + r_s_cm = CVT_GEOMETRY.secondary_centroid_radius(s) + r_s_cm_dot = CVT_GEOMETRY.secondary_outer_radius_time_derivative(s, s_dot) + phi_s = CVT_GEOMETRY.secondary_wrap_angle(s) + + helix_rotation = secondary_pulley.initial_rotation + secondary_pulley.helix_ramp.theta(s) + helix_rotation_rate = secondary_pulley.helix_ramp.dtheta_dx(s) + spring_torsion_term = secondary_pulley.spring_coeff_tors * helix_rotation * helix_rotation_rate + spring_comp_term = secondary_pulley.spring_coeff_comp * (secondary_pulley.initial_compression + s) + + mu_k = RUBBER_ALUMINUM_KINETIC_FRICTION + beta = SHEAVE_ANGLE / 2.0 + rho_b = RUBBER_DENSITY + A_b = BELT_CROSS_SECTIONAL_AREA + + sigma_s = decision.secondary_slip_direction + den_s = 1.0 - sigma_s * r_s * mu_k * math.tan(beta) * helix_rotation_rate + + traction_common = ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * r_s_cm_dot * v_b + ) + + numerator = ( + tau_engine / r_p + + I_p * r_p_dot * state.ω_p / (r_p ** 2) + - sigma_s * traction_common / den_s + ) + denominator = m_b + I_p / (r_p ** 2) - sigma_s * rho_b * A_b * phi_s * r_s_cm / den_s + v_b_dot = numerator / denominator + + tau_p = tau_engine - I_p * (v_b_dot - r_p_dot * state.ω_p) / r_p + tau_s = sigma_s * r_s * ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * (r_s_cm * v_b_dot + r_s_cm_dot * v_b) + ) / den_s + + return tau_p, tau_s + + +def both_slip_algebra( + decision: SlipMetricsResult, + state: SystemState, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, +) -> tuple[float, float]: + s = state.s + s_dot = state.s_dot + v_b = state.v_b + + r_p = decision.no_slip.breakdown.r_p + r_s = decision.no_slip.breakdown.r_s + + r_p_cm = CVT_GEOMETRY.primary_centroid_radius(s) + r_p_cm_dot = CVT_GEOMETRY.primary_outer_radius_time_derivative(s, s_dot) + r_s_cm = CVT_GEOMETRY.secondary_centroid_radius(s) + r_s_cm_dot = CVT_GEOMETRY.secondary_outer_radius_time_derivative(s, s_dot) + + phi_p = CVT_GEOMETRY.primary_wrap_angle(s) + phi_s = CVT_GEOMETRY.secondary_wrap_angle(s) + F_p_ax = primary_pulley.calculate_axial_clamping_force(state).pulley_breakdown.net + + helix_rotation = secondary_pulley.initial_rotation + secondary_pulley.helix_ramp.theta(s) + helix_rotation_rate = secondary_pulley.helix_ramp.dtheta_dx(s) + spring_torsion_term = secondary_pulley.spring_coeff_tors * helix_rotation * helix_rotation_rate + spring_comp_term = secondary_pulley.spring_coeff_comp * (secondary_pulley.initial_compression + s) + + mu_k = RUBBER_ALUMINUM_KINETIC_FRICTION + beta = SHEAVE_ANGLE / 2.0 + rho_b = RUBBER_DENSITY + A_b = BELT_CROSS_SECTIONAL_AREA + + sigma_p = decision.primary_slip_direction + sigma_s = decision.secondary_slip_direction + + den_s = 1.0 - sigma_s * r_s * mu_k * math.tan(beta) * helix_rotation_rate + if abs(den_s) < 1e-9: + den_s = math.copysign(1e-9, den_s if den_s != 0.0 else 1.0) + + primary_term = sigma_p * (2.0 * mu_k * F_p_ax * math.tan(beta) - rho_b * A_b * phi_p * r_p_cm_dot * v_b) + secondary_term = sigma_s * ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * r_s_cm_dot * v_b + ) / den_s + + numerator = primary_term - secondary_term + denominator = m_b + sigma_p * rho_b * A_b * phi_p * r_p_cm - sigma_s * rho_b * A_b * phi_s * r_s_cm / den_s + v_b_dot = numerator / denominator + + tau_p = sigma_p * r_p * ( + 2.0 * mu_k * F_p_ax * math.tan(beta) + - rho_b * A_b * phi_p * (r_p_cm * v_b_dot + r_p_cm_dot * v_b) + ) + tau_s = sigma_s * r_s * ( + mu_k * math.tan(beta) * spring_torsion_term + + 2.0 * mu_k * math.tan(beta) * spring_comp_term + - rho_b * A_b * phi_s * (r_s_cm * v_b_dot + r_s_cm_dot * v_b) + ) / den_s + + return tau_p, tau_s diff --git a/cvtModel/src/cvt_simulator/slip/branch_resolver.py b/cvtModel/src/cvt_simulator/slip/branch_resolver.py new file mode 100644 index 00000000..e34f57e4 --- /dev/null +++ b/cvtModel/src/cvt_simulator/slip/branch_resolver.py @@ -0,0 +1,129 @@ +"""Slip branch resolver. + +Takes the contact-state decision and returns the torques for the selected +branch. The selector provides kinematics and admissibility; this module turns +that into the discrete branch and resolves the branch algebra. +""" + +import math + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, SHEAVE_ANGLE +from cvt_simulator.constants.constants import RUBBER_ALUMINUM_KINETIC_FRICTION, RUBBER_DENSITY +from cvt_simulator.core.data_types import SlipMetricsResult, BranchTorqueResult, SlipBranch +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm +from cvt_simulator.core.slip.branch_algebra import ( + primary_slip_algebra, + secondary_slip_algebra, + both_slip_algebra, +) + + +class BranchResolver: + """Resolve branch choice into branch torques.""" + + def resolve_branch( + self, + slip_metrics: SlipMetricsResult, + state: SystemState, + tau_engine: float, + tau_load: float, + I_p: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> BranchTorqueResult: + """Return the torques for the selected branch.""" + branch = self._select_branch(slip_metrics) + no_slip = slip_metrics.no_slip + + if branch is SlipBranch.NO_SLIP: + return self._no_slip_branch(no_slip) + if branch is SlipBranch.PRIMARY_SLIP: + return self._primary_slip_branch(branch, slip_metrics, state, tau_load, I_s, m_b, primary_pulley) + if branch is SlipBranch.SECONDARY_SLIP: + return self._secondary_slip_branch(branch, slip_metrics, state, tau_engine, I_p, m_b, primary_pulley, secondary_pulley) + return self._both_slip_branch(branch, slip_metrics, state, tau_engine, tau_load, I_p, I_s, m_b, primary_pulley, secondary_pulley) + + def _select_branch(self, decision: SlipMetricsResult) -> SlipBranch: + # Stickability removed from the selector contract. Decide purely from admissibility. + if decision.primary_admissible and decision.secondary_admissible: + return SlipBranch.NO_SLIP + + if (not decision.primary_admissible) and decision.secondary_admissible: + return SlipBranch.PRIMARY_SLIP + + if decision.primary_admissible and (not decision.secondary_admissible): + return SlipBranch.SECONDARY_SLIP + + return SlipBranch.BOTH_SLIP + + def _no_slip_branch(self, no_slip: NoSlipResult) -> BranchTorqueResult: + return BranchTorqueResult( + branch=SlipBranch.NO_SLIP, + tau_p=no_slip.tau_p_ns, + tau_s=no_slip.tau_s_ns, + ) + + def _primary_slip_branch( + self, + branch: SlipBranch, + decision: SlipMetricsResult, + state: SystemState, + tau_load: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, + ) -> BranchTorqueResult: + tau_p, tau_s = primary_slip_algebra(decision, state, tau_load, I_s, m_b, primary_pulley) + return BranchTorqueResult( + branch=branch, + tau_p=tau_p, + tau_s=tau_s, + ) + + def _secondary_slip_branch( + self, + branch: SlipBranch, + decision: SlipMetricsResult, + state: SystemState, + tau_engine: float, + I_p: float, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> BranchTorqueResult: + tau_p, tau_s = secondary_slip_algebra( + decision, state, tau_engine, I_p, m_b, primary_pulley, secondary_pulley + ) + return BranchTorqueResult( + branch=branch, + tau_p=tau_p, + tau_s=tau_s, + ) + + def _both_slip_branch( + self, + branch: SlipBranch, + decision: SlipMetricsResult, + state: SystemState, + tau_engine: float, + tau_load: float, + I_p: float, + I_s: float, + m_b: float, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> BranchTorqueResult: + tau_p, tau_s = both_slip_algebra( + decision, state, tau_engine, tau_load, I_p, I_s, m_b, primary_pulley, secondary_pulley + ) + return BranchTorqueResult( + branch=branch, + tau_p=tau_p, + tau_s=tau_s, + ) \ No newline at end of file diff --git a/cvtModel/src/cvt_simulator/slip/contact_torque_solver.py b/cvtModel/src/cvt_simulator/slip/contact_torque_solver.py new file mode 100644 index 00000000..83958713 --- /dev/null +++ b/cvtModel/src/cvt_simulator/slip/contact_torque_solver.py @@ -0,0 +1,79 @@ +"""Public contact torque orchestration for the slip pipeline. + +This module wraps the no-slip candidate, torque admissibility evaluation, +branch selection, and branch resolution into one call that returns the +selected contact torques and belt acceleration. +""" + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.core.data_types import ContactTorqueResult +from cvt_simulator.sim_utils.system_state import SystemState + +from cvt_simulator.core.slip.branch_resolver import BranchResolver +from cvt_simulator.core.slip.slip_metrics import SlipMetrics +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult, compute_no_slip_candidate +from cvt_simulator.core.slip.torque_admissibility import TorqueAdmissibility + + +class ContactTorqueSolver: + """Resolve contact torques by selecting and solving the active slip branch.""" + + def __init__( + self, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> None: + self.primary_pulley = primary_pulley + self.secondary_pulley = secondary_pulley + self.torque_admissibility = TorqueAdmissibility(primary_pulley, secondary_pulley) + self.branch_selector = SlipMetrics() + self.branch_resolver = BranchResolver() + + def solve( + self, + state: SystemState, + tau_engine: float, + tau_load: float, + I_p: float, + I_s: float, + m_b: float, + ) -> ContactTorqueResult: + """Return the branch-selected contact torques and belt acceleration.""" + no_slip = compute_no_slip_candidate( + state=state, + τ_eng=tau_engine, + τ_load=tau_load, + I_p=I_p, + I_s=I_s, + m_b=m_b, + ) + + admissibility = self.torque_admissibility.get_breakdown(state, no_slip) + + # Contains no slip and admissibility objects + slip_metrics = self.branch_selector.decide_branch( + state=state, + no_slip=no_slip, + admissibility=admissibility, + ) + + branch_result = self.branch_resolver.resolve_branch( + slip_metrics=slip_metrics, + state=state, + tau_engine=tau_engine, + tau_load=tau_load, + I_p=I_p, + I_s=I_s, + m_b=m_b, + primary_pulley=self.primary_pulley, + secondary_pulley=self.secondary_pulley, + ) + + return ContactTorqueResult( + tau_p=branch_result.tau_p, + tau_s=branch_result.tau_s, + branch=branch_result.branch, + slip_metrics=slip_metrics, + branch_result=branch_result, + ) diff --git a/cvtModel/src/cvt_simulator/slip/torque_admissibility.py b/cvtModel/src/cvt_simulator/slip/torque_admissibility.py new file mode 100644 index 00000000..51c603ae --- /dev/null +++ b/cvtModel/src/cvt_simulator/slip/torque_admissibility.py @@ -0,0 +1,167 @@ +"""Torque admissibility equations. + +Implements the no-slip admissibility expressions for the primary and +secondary pulleys using the pulley component constants plus the no-slip +belt acceleration result. +""" +from cvt_simulator.core.data_types import ( + PrimaryTorqueAdmissibilityBreakdown, + SecondaryTorqueAdmissibilityBreakdown, + TorqueAdmissibilityResult, +) + +from cvt_simulator.geometry.cvt_geometry import CVT_GEOMETRY +import numpy as np + +from cvt_simulator.core.components.primary_pulley import PrimaryPulley +from cvt_simulator.core.components.secondary_pulley import SecondaryPulley +from cvt_simulator.constants.car_specs import BELT_CROSS_SECTIONAL_AREA, SHEAVE_ANGLE +from cvt_simulator.constants.constants import ( + RUBBER_ALUMINUM_KINETIC_FRICTION, + RUBBER_ALUMINUM_STATIC_FRICTION, + RUBBER_DENSITY, +) +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.core.slip.no_slip_candidate import NoSlipResult + + +class TorqueAdmissibility: + """ + Computes the bounds for the primary and secondary CVT + to determine if no-slip is admissible. + Computed based on the no slip result + """ + + def __init__( + self, + primary_pulley: PrimaryPulley, + secondary_pulley: SecondaryPulley, + ) -> None: + self.primary_pulley = primary_pulley + self.secondary_pulley = secondary_pulley + self.mu_static = RUBBER_ALUMINUM_STATIC_FRICTION + self.mu_kinetic = RUBBER_ALUMINUM_KINETIC_FRICTION + self.beta = SHEAVE_ANGLE / 2 + self.cvt = CVT_GEOMETRY + + def get_breakdown( + self, + state: SystemState, + no_slip: NoSlipResult, + ) -> TorqueAdmissibilityResult: + """Compute primary and secondary torque admissibility. + + Args: + state: Current system state. + no_slip: No-slip candidate result carrying v_b_dot_ns and torques. + + Returns: + TorqueAdmissibilityResult with explicit term breakdowns. + """ + primary_breakdown = self._primary_breakdown(state, no_slip) + secondary_breakdown = self._secondary_breakdown(state, no_slip) + + return TorqueAdmissibilityResult( + primary=primary_breakdown, + secondary=secondary_breakdown, + primary_tau_p_stick_upper=primary_breakdown.tau_p_stick_upper, + primary_tau_p_stick_lower=primary_breakdown.tau_p_stick_lower, + secondary_tau_stick_upper=secondary_breakdown.tau_stick_upper, + secondary_tau_stick_lower=secondary_breakdown.tau_stick_lower, + ) + + def _primary_breakdown( + self, + state: SystemState, + no_slip: NoSlipResult, + ) -> PrimaryTorqueAdmissibilityBreakdown: + s = state.s + + wrap_angle = self.cvt.primary_wrap_angle(s) + effective_radius = self.cvt.primary_effective_radius(s) + centroid_radius = self.cvt.primary_centroid_radius(s) + centroid_radius_rate = self.cvt.primary_outer_radius_time_derivative(s, state.s_dot) + # Use pulley-only clamping force (exclude belt centrifugal contribution) + axial_clamping_force = self.primary_pulley.calculate_axial_clamping_force(state).pulley_breakdown.net + + belt_centripetal_term = RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * wrap_angle * ( + centroid_radius * no_slip.v_b_dot_ns + centroid_radius_rate * state.v_b + ) + + base_limit = effective_radius * ( + 2.0 * self.mu_static * np.tan(self.beta) * axial_clamping_force + - belt_centripetal_term + ) + tau_p_stick_upper = base_limit + tau_p_stick_lower = -base_limit + + return PrimaryTorqueAdmissibilityBreakdown( + shift_distance=s, + wrap_angle=wrap_angle, + effective_radius=effective_radius, + centroid_radius=centroid_radius, + centroid_radius_rate=centroid_radius_rate, + axial_clamping_force=axial_clamping_force, + belt_centripetal_term=belt_centripetal_term, + friction_coefficient=self.mu_static, + sheave_half_angle=self.beta, + tau_p_stick_limit=base_limit, + tau_p_stick_upper=tau_p_stick_upper, + tau_p_stick_lower=tau_p_stick_lower, + ) + + def _secondary_breakdown( + self, + state: SystemState, + no_slip: NoSlipResult, + ) -> SecondaryTorqueAdmissibilityBreakdown: + s = state.s + + wrap_angle = self.cvt.secondary_wrap_angle(s) + effective_radius = self.cvt.secondary_effective_radius(s) + centroid_radius = self.cvt.secondary_centroid_radius(s) + centroid_radius_rate = self.cvt.secondary_outer_radius_time_derivative(s, state.s_dot) + + helix_rotation = self.secondary_pulley.initial_rotation + self.secondary_pulley.helix_ramp.theta(s) + helix_rotation_rate = self.secondary_pulley.helix_ramp.dtheta_dx(s) + + spring_torsion_term = self.secondary_pulley.spring_coeff_tors * helix_rotation * helix_rotation_rate + spring_comp_term = self.secondary_pulley.spring_coeff_comp * ( + self.secondary_pulley.initial_compression + s + ) + + belt_centripetal_term = RUBBER_DENSITY * BELT_CROSS_SECTIONAL_AREA * wrap_angle * ( + centroid_radius * no_slip.v_b_dot_ns + centroid_radius_rate * state.v_b + ) + + common_numerator = ( + self.mu_static * np.tan(self.beta) * spring_torsion_term + + 2.0 * self.mu_static * np.tan(self.beta) * spring_comp_term + - belt_centripetal_term + ) + numerator = effective_radius * common_numerator + + denominator_upper = 1.0 - effective_radius * self.mu_static * np.tan(self.beta) * helix_rotation_rate + denominator_lower = 1.0 + effective_radius * self.mu_static * np.tan(self.beta) * helix_rotation_rate + + tau_stick_upper = numerator / denominator_upper + tau_stick_lower = -numerator / denominator_lower + + return SecondaryTorqueAdmissibilityBreakdown( + shift_distance=s, + wrap_angle=wrap_angle, + effective_radius=effective_radius, + centroid_radius=centroid_radius, + centroid_radius_rate=centroid_radius_rate, + helix_rotation=helix_rotation, + helix_rotation_rate=helix_rotation_rate, + spring_torsion_term=spring_torsion_term, + spring_comp_term=spring_comp_term, + belt_centripetal_term=belt_centripetal_term, + friction_coefficient=self.mu_static, + sheave_half_angle=self.beta, + denominator_upper=denominator_upper, + denominator_lower=denominator_lower, + tau_stick_upper=tau_stick_upper, + tau_stick_lower=tau_stick_lower, + ) diff --git a/cvtModel/src/cvt_simulator/solvers/prim_engagement/primary_cvt_engagement_solver.py b/cvtModel/src/cvt_simulator/solvers/prim_engagement/primary_cvt_engagement_solver.py index f0f58f92..7068ff66 100644 --- a/cvtModel/src/cvt_simulator/solvers/prim_engagement/primary_cvt_engagement_solver.py +++ b/cvtModel/src/cvt_simulator/solvers/prim_engagement/primary_cvt_engagement_solver.py @@ -12,11 +12,12 @@ from scipy.optimize import brentq import matplotlib.pyplot as plt from cvt_simulator.solvers.solver_interface import SolverBase, SolverResult -from cvt_simulator.utils.simulation_args import SimulationArgs -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.models.model_initializer import get_models +from cvt_simulator.sim_utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.simulation_runner import SimulationRunner +from cvt_simulator.core.slip.no_slip_candidate import compute_no_slip_candidate +from cvt_simulator.slip.torque_admissibility import TorqueAdmissibility from cvt_simulator.utils.conversions import rad_s_to_rpm -from cvt_simulator.constants.car_specs import ENGINE_INERTIA class PrimaryCVTEngagementSolver(SolverBase): @@ -47,12 +48,15 @@ def __init__(self, args: SimulationArgs): args: Simulation parameters defining the CVT configuration """ super().__init__(args) - - # Initialize models to get the primary pulley and engine - system_model = get_models(args) - self.primary_pulley = system_model.cvt_shift_model.primary_pulley - self.engine_model = system_model.slip_model.engine_model - self.primary_inertia = ENGINE_INERTIA + self.contact_model = SimulationRunner.from_simulation_args(args).contact_model + self.primary_pulley = self.contact_model.primary_pulley + self.secondary_pulley = self.contact_model.secondary_pulley + self.engine_model = self.contact_model.engine_model + self.load_model = self.contact_model.load_model + self.torque_admissibility = TorqueAdmissibility( + self.primary_pulley, + self.secondary_pulley, + ) @property def solver_name(self) -> str: @@ -149,30 +153,26 @@ def _evaluate_t_max(self, angular_velocity: float) -> float: # Create a mock system state at minimum shift position (engagement) # At engagement, the CVT is at its lowest ratio (largest primary radius) state = SystemState( - primary_pulley_angular_velocity=angular_velocity, - secondary_pulley_angular_velocity=0.0, # Stationary - shift_distance=0.0, # Minimum shift position - shift_velocity=0.0, # Static evaluation + ω_p=angular_velocity, + ω_s=0.0, # Stationary + s=0.0, # Minimum shift position + s_dot=0.0, # Static evaluation ) # Get engine torque at this angular velocity engine_torque = self.engine_model.get_torque(angular_velocity) - - # Calculate torque bounds using the primary pulley model - # For primary pulley, we want tau_upper (the positive bound) - torque_bounds = self.primary_pulley.calculate_torque_bounds( - state, - engine_drive_torque=engine_torque, - primary_inertia=self.primary_inertia, - is_stick=True, - v_b_star=0.0, - T_b=1.0, + load_torque = self.load_model.get_breakdown(state).net_torque_at_secondary + + no_slip = compute_no_slip_candidate( + state=state, + τ_eng=engine_torque, + τ_load=load_torque, + I_p=self.contact_model.drivetrain_dynamics.I_p, + I_s=self.contact_model.drivetrain_dynamics.I_s, + m_b=self.contact_model.drivetrain_dynamics.m_b, ) - - # extract tau_upper from the bounds object - tau_upper = torque_bounds.tau_upper - - return tau_upper + admissibility = self.torque_admissibility.get_breakdown(state, no_slip) + return admissibility.primary_tau_p_stick_upper def get_engagement_curve( self, diff --git a/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_flyweight_mass.py b/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_flyweight_mass.py index 84ec378f..1be7402d 100644 --- a/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_flyweight_mass.py +++ b/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_flyweight_mass.py @@ -7,7 +7,7 @@ import numpy as np import matplotlib.pyplot as plt -from cvt_simulator.utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_args import SimulationArgs from cvt_simulator.solvers.shift_initiation.shift_initiation_solver import ( ShiftInitiationSolver, ) diff --git a/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_secondary_precompression.py b/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_secondary_precompression.py index 4a29e250..16ed38e1 100644 --- a/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_secondary_precompression.py +++ b/cvtModel/src/cvt_simulator/solvers/shift_initiation/analyze_secondary_precompression.py @@ -7,7 +7,7 @@ import numpy as np import matplotlib.pyplot as plt -from cvt_simulator.utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_args import SimulationArgs from cvt_simulator.solvers.shift_initiation.shift_initiation_solver import ( ShiftInitiationSolver, ) diff --git a/cvtModel/src/cvt_simulator/solvers/shift_initiation/shift_initiation_solver.py b/cvtModel/src/cvt_simulator/solvers/shift_initiation/shift_initiation_solver.py index 8d95e07a..b5fef1bc 100644 --- a/cvtModel/src/cvt_simulator/solvers/shift_initiation/shift_initiation_solver.py +++ b/cvtModel/src/cvt_simulator/solvers/shift_initiation/shift_initiation_solver.py @@ -13,9 +13,9 @@ from scipy.optimize import brentq import matplotlib.pyplot as plt from cvt_simulator.solvers.solver_interface import SolverBase, SolverResult -from cvt_simulator.utils.simulation_args import SimulationArgs -from cvt_simulator.utils.system_state import SystemState -from cvt_simulator.models.model_initializer import get_models +from cvt_simulator.sim_utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.simulation_runner import SimulationRunner from cvt_simulator.utils.conversions import rad_s_to_rpm @@ -47,15 +47,7 @@ def __init__(self, args: SimulationArgs): args: Simulation parameters defining the CVT configuration """ super().__init__(args) - - # Initialize models to get both pulleys and the shift model - system_model = get_models(args) - self.cvt_shift_model = system_model.cvt_shift_model - self.primary_pulley = system_model.cvt_shift_model.primary_pulley - self.secondary_pulley = system_model.cvt_shift_model.secondary_pulley - - # Get slip model for computing torque demand - self.slip_model = system_model.slip_model + self.contact_model = SimulationRunner.from_simulation_args(args).contact_model @property def solver_name(self) -> str: @@ -153,29 +145,15 @@ def _evaluate_force_difference(self, angular_velocity: float) -> float: """ # Create a system state at minimum shift position and stationary state = SystemState( - primary_pulley_angular_velocity=angular_velocity, - secondary_pulley_angular_velocity=0.0, # Stationary (as specified) - shift_distance=0.0, # Minimum shift position - shift_velocity=0.0, # Static evaluation - ) - - # Calculate torque demand from road load (before slip limiting) - torque_demand = self.slip_model.get_no_slip_torque(state) - - # Get the CVT breakdown which includes both pulley states - # Use torque_demand to properly account for secondary torque feedback - cvt_breakdown = self.cvt_shift_model.get_breakdown( - state, coupling_torque=torque_demand - ) - - # Extract axial clamping forces - primary_axial_force = cvt_breakdown.primaryPulleyState.forces.axial_force_total - secondary_axial_force = ( - cvt_breakdown.secondaryPulleyState.forces.axial_force_total + ω_p=angular_velocity, + ω_s=0.0, # Stationary (as specified) + s=0.0, # Minimum shift position + s_dot=0.0, # Static evaluation ) - # Return difference (positive means primary is winning, shift will occur) - return primary_axial_force - secondary_axial_force + # Return the shift net force directly from the current contact model. + # Positive means the primary side is winning and shift should initiate. + return self.contact_model.get_breakdown(state).shift.net def get_force_difference_curve( self, diff --git a/cvtModel/src/cvt_simulator/solvers/solve.py b/cvtModel/src/cvt_simulator/solvers/solve.py index 4ffff6cc..ad34e562 100644 --- a/cvtModel/src/cvt_simulator/solvers/solve.py +++ b/cvtModel/src/cvt_simulator/solvers/solve.py @@ -22,7 +22,7 @@ """ from dataclasses import dataclass -from cvt_simulator.utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_args import SimulationArgs from cvt_simulator.solvers.solver_interface import SolverResult from cvt_simulator.solvers.prim_engagement.primary_cvt_engagement_solver import ( PrimaryCVTEngagementSolver, diff --git a/cvtModel/src/cvt_simulator/solvers/solver_interface.py b/cvtModel/src/cvt_simulator/solvers/solver_interface.py index a4f4f35d..10fb0344 100644 --- a/cvtModel/src/cvt_simulator/solvers/solver_interface.py +++ b/cvtModel/src/cvt_simulator/solvers/solver_interface.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Optional -from cvt_simulator.utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_args import SimulationArgs @dataclass diff --git a/cvtModel/src/cvt_simulator/utils/diagnostics/solver_slowdown_diagnostic.py b/cvtModel/src/cvt_simulator/test_scripts/diagnostics/solver_slowdown_diagnostic.py similarity index 94% rename from cvtModel/src/cvt_simulator/utils/diagnostics/solver_slowdown_diagnostic.py rename to cvtModel/src/cvt_simulator/test_scripts/diagnostics/solver_slowdown_diagnostic.py index d27fbc09..f02d1eff 100644 --- a/cvtModel/src/cvt_simulator/utils/diagnostics/solver_slowdown_diagnostic.py +++ b/cvtModel/src/cvt_simulator/test_scripts/diagnostics/solver_slowdown_diagnostic.py @@ -15,15 +15,14 @@ import time import numpy as np -from cvt_simulator.models.model_initializer import get_models -from cvt_simulator.models.ramps.ramp_config import ( +from cvt_simulator.ramps.ramp_config import ( CircularSegmentConfig, LinearSegmentConfig, PiecewiseRampConfig, ) from cvt_simulator.simulation_runner import SimulationRunner -from cvt_simulator.utils.simulation_args import SimulationArgs -from cvt_simulator.utils.simulation_constraints import ( +from cvt_simulator.sim_utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_constraints import ( get_mid_shift_steady_event, get_mid_shift_wake_event, ) @@ -119,7 +118,6 @@ def run_single_case( ) -> dict: print(f"\n=== Input Case: {case_name} ===") print(args) - system_model = get_models(args) progress_state = { "start_wall": time.perf_counter(), "last_wall": 0.0, @@ -177,8 +175,8 @@ def transition_callback(payload: dict): flush=True, ) - runner = SimulationRunner( - system_model, + runner = SimulationRunner.from_simulation_args( + args, progress_callback=progress_callback, transition_callback=transition_callback, ) @@ -251,11 +249,11 @@ def transition_callback(payload: dict): shift_velocity = [] shift_distance = [] for state in result.states: - breakdown = system_model.get_breakdown(state) - net_axial.append(breakdown.cvt_dynamics.net) - shift_accel.append(breakdown.cvt_dynamics.acceleration) - shift_velocity.append(state.shift_velocity) - shift_distance.append(state.shift_distance) + breakdown = runner.contact_model.get_breakdown(state) + net_axial.append(breakdown.shift.net) + shift_accel.append(breakdown.shift.acceleration) + shift_velocity.append(state.s_dot) + shift_distance.append(state.s) net_axial_arr = np.asarray(net_axial) shift_accel_arr = np.asarray(shift_accel) @@ -327,8 +325,8 @@ def transition_callback(payload: dict): print(f"intervals < 1e-2s: {int(np.sum(transition_dt < 1e-2))}") print(f"intervals < 1e-3s: {int(np.sum(transition_dt < 1e-3))}") - mid_shift_steady_event = get_mid_shift_steady_event(system_model) - mid_shift_wake_event = get_mid_shift_wake_event(system_model) + mid_shift_steady_event = get_mid_shift_steady_event(runner.contact_model) + mid_shift_wake_event = get_mid_shift_wake_event(runner.contact_model) print("\n=== Transition Threshold Probes ===") for i, tr in enumerate(transitions, start=1): diff --git a/cvtModel/src/cvt_simulator/utils/generate_graphs.py b/cvtModel/src/cvt_simulator/test_scripts/generate_graphs.py similarity index 91% rename from cvtModel/src/cvt_simulator/utils/generate_graphs.py rename to cvtModel/src/cvt_simulator/test_scripts/generate_graphs.py index 62656282..13ff8f46 100644 --- a/cvtModel/src/cvt_simulator/utils/generate_graphs.py +++ b/cvtModel/src/cvt_simulator/test_scripts/generate_graphs.py @@ -5,29 +5,29 @@ import numpy as np from cvt_simulator.constants.car_specs import GEARBOX_RATIO, MAX_SHIFT, WHEEL_RADIUS -from cvt_simulator.utils.simulation_result import SimulationResult +from cvt_simulator.sim_utils.simulation_result import SimulationResult from cvt_simulator.utils.state_computations import ( integrate_positions_trapezoidal, secondary_pulley_angular_velocity_to_car_velocity, ) -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm def _build_series(result: SimulationResult) -> dict[str, np.ndarray]: time = np.asarray(result.time) - shift_distance = np.asarray([state.shift_distance for state in result.states]) - shift_velocity = np.asarray([state.shift_velocity for state in result.states]) + shift_distance = np.asarray([state.s for state in result.states]) + shift_velocity = np.asarray([state.s_dot for state in result.states]) primary_omega = np.asarray( - [state.primary_pulley_angular_velocity for state in result.states] + [state.ω_p for state in result.states] ) secondary_omega = np.asarray( - [state.secondary_pulley_angular_velocity for state in result.states] + [state.ω_s for state in result.states] ) car_velocity = np.asarray( [ secondary_pulley_angular_velocity_to_car_velocity( - state.secondary_pulley_angular_velocity + state.ω_s ) for state in result.states ] diff --git a/cvtModel/src/cvt_simulator/utils/ramp_utils.py b/cvtModel/src/cvt_simulator/test_scripts/ramp_utils.py similarity index 100% rename from cvtModel/src/cvt_simulator/utils/ramp_utils.py rename to cvtModel/src/cvt_simulator/test_scripts/ramp_utils.py diff --git a/cvtModel/src/cvt_simulator/validation/.gitignore b/cvtModel/src/cvt_simulator/test_scripts/validation/.gitignore similarity index 100% rename from cvtModel/src/cvt_simulator/validation/.gitignore rename to cvtModel/src/cvt_simulator/test_scripts/validation/.gitignore diff --git a/cvtModel/src/cvt_simulator/validation/validate.py b/cvtModel/src/cvt_simulator/test_scripts/validation/validate.py similarity index 100% rename from cvtModel/src/cvt_simulator/validation/validate.py rename to cvtModel/src/cvt_simulator/test_scripts/validation/validate.py diff --git a/cvtModel/src/cvt_simulator/utils/frontend_output.py b/cvtModel/src/cvt_simulator/utils/frontend_output.py index a7352336..62694268 100644 --- a/cvtModel/src/cvt_simulator/utils/frontend_output.py +++ b/cvtModel/src/cvt_simulator/utils/frontend_output.py @@ -1,38 +1,43 @@ -from typing import List, Dict -from cvt_simulator.constants.car_specs import MAX_SHIFT -from cvt_simulator.models.dataTypes import DrivetrainBreakdown -from cvt_simulator.utils.system_state import SystemState +from __future__ import annotations + +from dataclasses import dataclass, fields, is_dataclass +from typing import Dict, List + import pandas as pd -from cvt_simulator.models.model_initializer import get_models -from cvt_simulator.utils.simulation_args import SimulationArgs -from cvt_simulator.utils.simulation_result import SimulationResult + +from cvt_simulator.constants.car_specs import MAX_SHIFT +from cvt_simulator.core.data_types import ContactDynamicsBreakdown +from cvt_simulator.sim_utils.system_state import SystemState +from cvt_simulator.simulation_runner import SimulationRunner +from cvt_simulator.sim_utils.simulation_args import SimulationArgs +from cvt_simulator.sim_utils.simulation_result import SimulationResult from cvt_simulator.utils.state_computations import ( integrate_positions_trapezoidal, primary_pulley_angular_velocity_to_engine_angular_velocity, secondary_pulley_angular_velocity_to_car_velocity, ) -from dataclasses import is_dataclass, fields, dataclass @dataclass -class TimeStepData: - """ - Represents all the data for a single time step in the simulation. - Uses the unified DrivetrainBreakdown for clean access to all component data. - """ +class AnalysisStepData: + """All analysis data for one simulation time step.""" time: float state: SystemState derived_state: "DerivedKinematicState" - drivetrain: DrivetrainBreakdown + contact_breakdown: ContactDynamicsBreakdown + + +TimeStepData = AnalysisStepData @dataclass class DerivedKinematicState: - """Derived kinematic signals that are not part of the 4 DOF solver state.""" + """Kinematic signals derived from the solver state.""" car_velocity: float car_position: float + belt_position: float engine_angular_velocity: float engine_angular_position: float @@ -50,19 +55,21 @@ class SimulationTerminationContext: event_time: float | None transition_count: int max_transitions: int - details: Dict[str, float | str | bool] + details: Dict[str, float | str | bool | int] + +class SimulationAnalysisResult: + """Analysis-oriented projection of a simulation run.""" -class FormattedSimulationResult: - data: List[TimeStepData] + # Explicit response contract for backend auto model generation. + data: List[AnalysisStepData] termination: SimulationTerminationContext def __init__(self, result: SimulationResult, args: SimulationArgs): - """ - Initialize using the base SimulationResult and then compute additional columns. - """ - self.data = [] - self.gather_model_states(result, args) + contact_model = SimulationRunner.from_simulation_args(args).contact_model + self.rows: List[AnalysisStepData] = [] + self.data = self.rows + self.gather_model_states(result, contact_model) self.termination = self._build_termination_context(result) @staticmethod @@ -71,7 +78,7 @@ def _build_termination_context( ) -> SimulationTerminationContext: context = result.termination_context or {} details_raw = context.get("details", {}) - details: Dict[str, float | str | bool] = {} + details: Dict[str, float | str | bool | int] = {} if isinstance(details_raw, dict): details = {str(k): v for k, v in details_raw.items()} @@ -98,139 +105,116 @@ def _build_termination_context( details=details, ) - def gather_model_states(self, result: SimulationResult, args: SimulationArgs): - system_model = get_models(args) - + def gather_model_states(self, result: SimulationResult, contact_model): car_velocities = [ - secondary_pulley_angular_velocity_to_car_velocity( - state.secondary_pulley_angular_velocity - ) + secondary_pulley_angular_velocity_to_car_velocity(state.ω_s) for state in result.states ] engine_angular_velocities = [ - primary_pulley_angular_velocity_to_engine_angular_velocity( - state.primary_pulley_angular_velocity - ) + primary_pulley_angular_velocity_to_engine_angular_velocity(state.ω_p) for state in result.states ] - primary_pulley_angular_velocities = [ - state.primary_pulley_angular_velocity for state in result.states - ] - secondary_pulley_angular_velocities = [ - state.secondary_pulley_angular_velocity for state in result.states - ] + car_positions = integrate_positions_trapezoidal(result.time, car_velocities) + belt_positions = integrate_positions_trapezoidal( + result.time, [state.v_b for state in result.states] + ) engine_positions = integrate_positions_trapezoidal( result.time, engine_angular_velocities ) - primary_pulley_angular_positions = integrate_positions_trapezoidal( - result.time, primary_pulley_angular_velocities - ) - secondary_pulley_angular_positions = integrate_positions_trapezoidal( - result.time, secondary_pulley_angular_velocities - ) - - for i, (time, state) in enumerate(zip(result.time, result.states)): - shift_velocity = state.shift_velocity - shift_distance = state.shift_distance - if shift_distance <= 0: - state.shift_distance = 0 - state.shift_velocity = max(0, shift_velocity) - - elif shift_distance > MAX_SHIFT: - state.shift_distance = MAX_SHIFT - state.shift_velocity = min(0, shift_velocity) - - drivetrain_breakdown = system_model.get_breakdown(state) - # The 4-DOF solver tracks angular velocities only; integrate them over time - # so frontend consumers can animate pulley angular position directly. - drivetrain_breakdown.cvt_dynamics.primaryPulleyState.angular_position = ( - float(primary_pulley_angular_positions[i]) - ) - drivetrain_breakdown.cvt_dynamics.secondaryPulleyState.angular_position = ( - float(secondary_pulley_angular_positions[i]) + for index, (time, state) in enumerate(zip(result.time, result.states)): + display_state = SystemState( + s=float(state.s), + s_dot=float(state.s_dot), + ω_p=float(state.ω_p), + ω_s=float(state.ω_s), + v_b=float(state.v_b), ) + if display_state.s <= 0.0: + display_state.s = 0.0 + display_state.s_dot = max(0.0, display_state.s_dot) + elif display_state.s > MAX_SHIFT: + display_state.s = float(MAX_SHIFT) + display_state.s_dot = min(0.0, display_state.s_dot) + + contact_breakdown = contact_model.get_breakdown(display_state) + # Sanitize enum values to primitives to avoid retaining Enum internals + try: + # Convert top-level branch enum to its name + if hasattr(contact_breakdown, "contact") and getattr(contact_breakdown, "contact") is not None: + contact = contact_breakdown.contact + if hasattr(contact, "branch") and contact.branch is not None: + contact.branch = contact.branch.name + if hasattr(contact, "branch_result") and getattr(contact, "branch_result") is not None: + br = contact.branch_result + if hasattr(br, "branch") and br.branch is not None: + br.branch = br.branch.name + except Exception: + # Best-effort sanitization; failure is non-fatal + pass + derived_state = DerivedKinematicState( - car_velocity=car_velocities[i], - car_position=float(car_positions[i]), - engine_angular_velocity=engine_angular_velocities[i], - engine_angular_position=float(engine_positions[i]), + car_velocity=car_velocities[index], + car_position=float(car_positions[index]), + belt_position=float(belt_positions[index]), + engine_angular_velocity=engine_angular_velocities[index], + engine_angular_position=float(engine_positions[index]), ) - time_step_data = TimeStepData( - time=time, - state=state, - derived_state=derived_state, - drivetrain=drivetrain_breakdown, + self.rows.append( + AnalysisStepData( + time=float(time), + state=display_state, + derived_state=derived_state, + contact_breakdown=contact_breakdown, + ) ) - self.data.append(time_step_data) @staticmethod - def from_csv(filename="simulation_output.csv", args: SimulationArgs = None): - """ - Reads the simulation states from a CSV file and returns a FormattedSimulationResult instance. - Note: args parameter is required to compute car_state and cvt_state breakdowns. - """ + def from_csv(filename: str = "simulation_output.csv", args: SimulationArgs | None = None): + """Read states from CSV and reconstruct the analysis view.""" base_result = SimulationResult.from_csv(filename) if args is None: raise ValueError("SimulationArgs is required to compute model breakdowns") - return FormattedSimulationResult(base_result, args) + return SimulationAnalysisResult(base_result, args) - def write_formatted_csv(self, filename="front_end_output.csv"): - """ - Flattens the data and writes to a CSV file for front-end consumption. - """ - # Get all unique keys from all time steps by flattening the entire TimeStepData + def write_analysis_csv(self, filename: str = "front_end_output.csv"): + """Flatten the analysis rows to CSV for downstream tooling.""" all_keys = set() + for row in self.rows: + flat_row = self._flatten_dataclass(row) + all_keys.update(flat_row.keys()) - # Collect all unique keys from all time steps - for time_step in self.data: - flat_time_step = self._flatten_dataclass(time_step) - all_keys.update(flat_time_step.keys()) - - # Initialize all columns - data = {} - for key in all_keys: - data[key] = [] - - # Populate all the flattened data - for time_step in self.data: - flat_time_step = self._flatten_dataclass(time_step) - - # For each key, append the value or None if missing + data = {key: [] for key in all_keys} + for row in self.rows: + flat_row = self._flatten_dataclass(row) for key in all_keys: - if key in flat_time_step: - data[key].append(flat_time_step[key]) - else: - data[key].append(None) + data[key].append(flat_row.get(key)) + + pd.DataFrame(data).to_csv(filename, index=False) - df = pd.DataFrame(data) - df.to_csv(filename, index=False) + def write_formatted_csv(self, filename: str = "front_end_output.csv"): + """Backward-compatible alias for the old formatter name.""" + self.write_analysis_csv(filename) - def _flatten_dataclass(self, obj, prefix=""): - """ - Recursively flatten a dataclass object into a flat dictionary. - """ + def _flatten_dataclass(self, obj, prefix: str = ""): + """Recursively flatten a dataclass object into a flat dictionary.""" flat_dict = {} if is_dataclass(obj): for field in fields(obj): - field_name = field.name - field_value = getattr(obj, field_name) - - # Create the key name with prefix - key = f"{prefix}_{field_name}" if prefix else field_name - - # Recursively flatten if it's another dataclass + field_value = getattr(obj, field.name) + key = f"{prefix}_{field.name}" if prefix else field.name if is_dataclass(field_value): - nested_dict = self._flatten_dataclass(field_value, key) - flat_dict.update(nested_dict) + flat_dict.update(self._flatten_dataclass(field_value, key)) else: flat_dict[key] = field_value else: - # If it's not a dataclass, just return it with the prefix as key flat_dict[prefix] = obj return flat_dict + + +FormattedSimulationResult = SimulationAnalysisResult diff --git a/cvtModel/src/cvt_simulator/utils/simulation_constraints.py b/cvtModel/src/cvt_simulator/utils/simulation_constraints.py index b4615e8b..10704296 100644 --- a/cvtModel/src/cvt_simulator/utils/simulation_constraints.py +++ b/cvtModel/src/cvt_simulator/utils/simulation_constraints.py @@ -1,11 +1,11 @@ from cvt_simulator.constants.car_specs import ( MAX_SHIFT, ) -from cvt_simulator.models.system_model import SystemModel +from cvt_simulator.core.dynamics.contact_dynamics_model import ContactDynamicsModel from cvt_simulator.utils.state_computations import ( secondary_pulley_angular_velocity_to_car_velocity, ) -from cvt_simulator.utils.system_state import SystemState +from cvt_simulator.sim_utils.system_state import SystemState MIN_CAR_VELOCITY_MPS = -20.0 @@ -18,16 +18,16 @@ def update_y(y, state: SystemState): def shift_constraint_event(t, y): state = SystemState.from_array(y) - shift_velocity = state.shift_velocity - shift_distance = state.shift_distance + shift_velocity = state.s_dot + shift_distance = state.s if shift_distance < 0: - state.shift_distance = 0 - state.shift_velocity = max(0, shift_velocity) + state.s = 0 + state.s_dot = max(0, shift_velocity) elif shift_distance > MAX_SHIFT: - state.shift_distance = MAX_SHIFT - state.shift_velocity = min(0, shift_velocity) + state.s = MAX_SHIFT + state.s_dot = min(0, shift_velocity) update_y(y, state) return 1 @@ -37,7 +37,7 @@ def car_velocity_constraint_event(t, y): state = SystemState.from_array(y) return ( secondary_pulley_angular_velocity_to_car_velocity( - state.secondary_pulley_angular_velocity + state.ω_s ) - MIN_CAR_VELOCITY_MPS ) @@ -47,7 +47,7 @@ def car_velocity_constraint_event(t, y): car_velocity_constraint_event.direction = -1 -def get_shift_steady_event(system_model: SystemModel): +def get_shift_steady_event(contact_model: ContactDynamicsModel): """ Returns an event function that triggers only when: 1. The system is close enough to full shift (i.e. shift_distance within tol of MAX_SHIFT). @@ -60,36 +60,33 @@ def shift_steady_event(t, y): tol = 1e-5 # Tolerance for proximity to MAX_SHIFT # Before we get near full shift, return a fixed negative value. - if state.shift_distance < MAX_SHIFT - tol: + if state.s < MAX_SHIFT - tol: return -tol # Clamp here as clamping from other events doesn't propagate immediately - shift_velocity = state.shift_velocity - shift_distance = state.shift_distance + shift_velocity = state.s_dot + shift_distance = state.s if shift_distance < 0: - state.shift_distance = 0 - state.shift_velocity = max(0, shift_velocity) + state.s = 0 + state.s_dot = max(0, shift_velocity) elif shift_distance > MAX_SHIFT: - state.shift_distance = MAX_SHIFT - state.shift_velocity = min(0, shift_velocity) + state.s = MAX_SHIFT + state.s_dot = min(0, shift_velocity) update_y(y, state) # Once near full shift, return the computed shift acceleration. # The event will trigger when this value crosses from negative to positive. # TODO: Clean this up! - coupling_torque = system_model.slip_model.get_breakdown(state).coupling_torque - return system_model.cvt_shift_model.get_breakdown( - state, coupling_torque - ).acceleration + return contact_model.get_breakdown(state).shift.acceleration shift_steady_event.terminal = True shift_steady_event.direction = 1 # Looking for a negative-to-positive crossing. return shift_steady_event -def get_back_shift_event(system_model: SystemModel): +def get_back_shift_event(contact_model: ContactDynamicsModel): """ Returns an event function that triggers when the system wants to back-shift from full shift position. This detects when the shift acceleration becomes @@ -100,14 +97,11 @@ def back_shift_event(t, y): state = SystemState.from_array(y) # Should only trigger when at full shift - if state.shift_distance < MAX_SHIFT - 1e-5: + if state.s < MAX_SHIFT - 1e-5: return 1.0 # Return positive value when not at full shift # Calculate the shift acceleration - coupling_torque = system_model.slip_model.get_breakdown(state).coupling_torque - shift_accel = system_model.cvt_shift_model.get_breakdown( - state, coupling_torque - ).acceleration + shift_accel = contact_model.get_breakdown(state).shift.acceleration # Return the acceleration + small threshold # Event triggers when this crosses from positive to negative @@ -120,7 +114,7 @@ def back_shift_event(t, y): def get_mid_shift_steady_event( - system_model: SystemModel, + contact_model: ContactDynamicsModel, velocity_tol: float = 1e-4, accel_tol: float = 0.1, wake_accel_guard_tol: float = 0.5, @@ -144,34 +138,27 @@ def mid_shift_steady_event(t, y): # Only apply in the interior region, not near hard shift boundaries. if ( - state.shift_distance <= boundary_margin - or state.shift_distance >= MAX_SHIFT - boundary_margin + state.s <= boundary_margin + or state.s >= MAX_SHIFT - boundary_margin ): return 1.0 - coupling_torque = system_model.slip_model.get_breakdown(state).coupling_torque - shift_accel = system_model.cvt_shift_model.get_breakdown( - state, coupling_torque - ).acceleration + shift_breakdown = contact_model.get_breakdown(state).shift + shift_accel = shift_breakdown.acceleration # Guard against immediate wake chatter: only lock if the locked-state # acceleration would also remain below the wake threshold. locked_state = SystemState( - shift_distance=state.shift_distance, - shift_velocity=0.0, - primary_pulley_angular_velocity=state.primary_pulley_angular_velocity, - secondary_pulley_angular_velocity=state.secondary_pulley_angular_velocity, + s=state.s, + s_dot=0.0, + ω_p=state.ω_p, + ω_s=state.ω_s, ) - locked_coupling_torque = system_model.slip_model.get_breakdown( - locked_state - ).coupling_torque - locked_shift_accel = system_model.cvt_shift_model.get_breakdown( - locked_state, locked_coupling_torque - ).acceleration + locked_shift_accel = contact_model.get_breakdown(locked_state).shift.acceleration # Deterministic event value: <= 0 means quasi-static and eligible to lock. return max( - abs(state.shift_velocity) - velocity_tol, + abs(state.s_dot) - velocity_tol, abs(shift_accel) - accel_tol, abs(locked_shift_accel) - wake_accel_guard_tol, ) @@ -184,7 +171,7 @@ def mid_shift_steady_event(t, y): def get_mid_shift_wake_event( - system_model: SystemModel, + contact_model: ContactDynamicsModel, wake_accel_tol: float = 1.5, boundary_margin: float = 1e-5, ): @@ -204,13 +191,10 @@ def get_mid_shift_wake_event( def mid_shift_wake_event(t, y): state = SystemState.from_array(y) - coupling_torque = system_model.slip_model.get_breakdown(state).coupling_torque - shift_accel = system_model.cvt_shift_model.get_breakdown( - state, coupling_torque - ).acceleration - if state.shift_distance <= boundary_margin: + shift_accel = contact_model.get_breakdown(state).shift.acceleration + if state.s <= boundary_margin: return shift_accel - wake_accel_tol - if state.shift_distance >= MAX_SHIFT - boundary_margin: + if state.s >= MAX_SHIFT - boundary_margin: return -shift_accel - wake_accel_tol return abs(shift_accel) - wake_accel_tol diff --git a/cvtModel/test/simulations/test_secondary_pulley.py b/cvtModel/test/simulations/test_secondary_pulley.py index 1d80605b..534e661a 100644 --- a/cvtModel/test/simulations/test_secondary_pulley.py +++ b/cvtModel/test/simulations/test_secondary_pulley.py @@ -2,7 +2,7 @@ import numpy as np from simulations.secondary_pulley import SecondaryPulley -from cvt_simulator.utils.theoretical_models import TheoreticalModels as tm +from cvt_simulator.geometry.theoretical_models import TheoreticalModels as tm from cvt_simulator.constants.car_specs import BELT_HEIGHT diff --git a/cvtModel/test/utils/test_ramp_config.py b/cvtModel/test/utils/test_ramp_config.py index bdad96ea..76470e6f 100644 --- a/cvtModel/test/utils/test_ramp_config.py +++ b/cvtModel/test/utils/test_ramp_config.py @@ -1,6 +1,6 @@ import unittest -from cvt_simulator.models.ramps.ramp_config import PiecewiseRampConfig +from cvt_simulator.ramps.ramp_config import PiecewiseRampConfig class TestRampConfigParsing(unittest.TestCase): diff --git a/cvtModel/test/utils/test_simulation_result.py b/cvtModel/test/utils/test_simulation_result.py index b20af52e..25ec28fc 100644 --- a/cvtModel/test/utils/test_simulation_result.py +++ b/cvtModel/test/utils/test_simulation_result.py @@ -4,8 +4,8 @@ from unittest.mock import patch from scipy.integrate import solve_ivp -from cvt_simulator.utils.simulation_result import SimulationResult -from cvt_simulator.utils.system_state import SystemState +from cvt_simulator.sim_utils.simulation_result import SimulationResult +from cvt_simulator.sim_utils.system_state import SystemState class TestSimulationResult(unittest.TestCase): @@ -24,10 +24,10 @@ def simple_ode(t, y): # Mock SystemState.from_array to return a simple object self.original_from_array = SystemState.from_array SystemState.from_array = lambda arr: SystemState( - shift_distance=arr[0], - shift_velocity=arr[1], - primary_pulley_angular_velocity=arr[2], - secondary_pulley_angular_velocity=arr[3], + s=arr[0], + s_dot=arr[1], + ω_p=arr[2], + ω_s=arr[3], ) def tearDown(self): diff --git a/cvtModel/test/utils/test_system_state.py b/cvtModel/test/utils/test_system_state.py index 4ec30ed8..d7e8997a 100644 --- a/cvtModel/test/utils/test_system_state.py +++ b/cvtModel/test/utils/test_system_state.py @@ -1,6 +1,6 @@ import unittest -from cvt_simulator.utils.system_state import SystemState +from cvt_simulator.sim_utils.system_state import SystemState from cvt_simulator.constants.car_specs import MAX_SHIFT @@ -8,24 +8,24 @@ class TestSystemState(unittest.TestCase): def test_initialization(self): state = SystemState( - shift_distance=2.0, - shift_velocity=5.0, - primary_pulley_angular_velocity=100.0, - secondary_pulley_angular_velocity=30.0, + s=2.0, + s_dot=5.0, + ω_p=100.0, + ω_s=30.0, v_b=12.0, ) - self.assertEqual(state.shift_distance, 2.0) - self.assertEqual(state.shift_velocity, 5.0) - self.assertEqual(state.primary_pulley_angular_velocity, 100.0) - self.assertEqual(state.secondary_pulley_angular_velocity, 30.0) + self.assertEqual(state.s, 2.0) + self.assertEqual(state.s_dot, 5.0) + self.assertEqual(state.ω_p, 100.0) + self.assertEqual(state.ω_s, 30.0) self.assertEqual(state.v_b, 12.0) def test_to_array(self): state = SystemState( - shift_distance=2.0, - shift_velocity=5.0, - primary_pulley_angular_velocity=100.0, - secondary_pulley_angular_velocity=30.0, + s=2.0, + s_dot=5.0, + ω_p=100.0, + ω_s=30.0, v_b=12.0, ) expected_array = [2.0, 5.0, 100.0, 30.0, 12.0] @@ -35,20 +35,20 @@ def test_from_array(self): array = [MAX_SHIFT, 5.0, 100.0, 30.0, 12.0] state = SystemState.from_array(array) # Shift distance is clamped when fetching it - self.assertEqual(state.shift_distance, MAX_SHIFT) - self.assertEqual(state.shift_velocity, 5.0) - self.assertEqual(state.primary_pulley_angular_velocity, 100.0) - self.assertEqual(state.secondary_pulley_angular_velocity, 30.0) + self.assertEqual(state.s, MAX_SHIFT) + self.assertEqual(state.s_dot, 5.0) + self.assertEqual(state.ω_p, 100.0) + self.assertEqual(state.ω_s, 30.0) self.assertEqual(state.v_b, 12.0) def test_from_array_with_legacy_state_vector(self): array = [MAX_SHIFT, 5.0, 100.0, 30.0] state = SystemState.from_array(array) - self.assertEqual(state.shift_distance, MAX_SHIFT) - self.assertEqual(state.shift_velocity, 5.0) - self.assertEqual(state.primary_pulley_angular_velocity, 100.0) - self.assertEqual(state.secondary_pulley_angular_velocity, 30.0) + self.assertEqual(state.s, MAX_SHIFT) + self.assertEqual(state.s_dot, 5.0) + self.assertEqual(state.ω_p, 100.0) + self.assertEqual(state.ω_s, 30.0) self.assertEqual(state.v_b, 0.0) diff --git a/cvtModel/test/utils/test_theoretical_models.py b/cvtModel/test/utils/test_theoretical_models.py index 71c0965c..4f33f7e7 100644 --- a/cvtModel/test/utils/test_theoretical_models.py +++ b/cvtModel/test/utils/test_theoretical_models.py @@ -1,6 +1,6 @@ import unittest -from cvt_simulator.utils.theoretical_models import TheoreticalModels +from cvt_simulator.geometry.theoretical_models import TheoreticalModels class TestTheoreticalModels(unittest.TestCase): diff --git a/docs/CVT_Module_Formulation/CVT_Module_Formulation.pdf b/docs/CVT_Module_Formulation/CVT_Module_Formulation.pdf index a7062321..9251eaed 100644 Binary files a/docs/CVT_Module_Formulation/CVT_Module_Formulation.pdf and b/docs/CVT_Module_Formulation/CVT_Module_Formulation.pdf differ diff --git a/docs/CVT_Module_Formulation/CVT_Module_Formulation.tex b/docs/CVT_Module_Formulation/CVT_Module_Formulation.tex index 1e8a6f98..3416a258 100644 --- a/docs/CVT_Module_Formulation/CVT_Module_Formulation.tex +++ b/docs/CVT_Module_Formulation/CVT_Module_Formulation.tex @@ -45,6 +45,7 @@ \setlist[enumerate]{after=\noindent} \usepackage{parskip} +\usepackage{changepage} \usepackage{placeins} \usepackage[round]{natbib} @@ -177,6 +178,35 @@ \end{tcolorbox} } +% ========================= +% Wide boxed equation sized to the equation width and centered +% Usage: \widebigeq{eq:key}{Title}{} +\newlength{\bigeqwidth} +\NewDocumentCommand{\widebigeq}{m m m}{% + % Measure natural width of the displayed equation + \begingroup + \setbox0=\hbox{\(\displaystyle #3\)}% + \setlength{\bigeqwidth}{\wd0}% + % Add small safety buffer to account for delimiter sizing differences + \addtolength{\bigeqwidth}{2em}% + % Step equation counter and set label + \refstepcounter{equation}\label{#1}% + + % Center a box of the measured width; if it is wider than \linewidth + % TeX will allow symmetric overflow because the glue becomes negative. + \noindent\hbox to \linewidth{\hss + \begin{tcolorbox}[width=\bigeqwidth,boxrule=0.6pt,left=8pt,right=8pt,top=8pt,bottom=8pt] + \noindent\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}l r@{}} + \textbf{#2} & \textbf{(\theequation)} + \end{tabular*} + \vspace{6pt} + \rule{\linewidth}{0.4pt} + \vspace{8pt} + \makebox[\linewidth][c]{\(\displaystyle #3\)}% + \end{tcolorbox} + \hss}\endgroup +} + % ========================= % Assumption block % ========================= @@ -1111,7 +1141,7 @@ \subsection{Symbol Summary}\label{sec:symbols} $\omega_p,\ \omega_s$ & primary and secondary angular speed \\ $s,\ \dot s,\ \ddot s$ & shift position, velocity, and acceleration \\ $R,\ \dot R$ & CVT ratio and ratio rate \\ -$\hat v_b,\ \dot{\hat v}_b$ & modeled belt transport speed and its time derivative \\ +$v_b$ & belt transport speed \\ $v_b^\ast,\ T_b$ & slip-branch target belt speed and belt-speed relaxation time constant \\ $v_{\Delta}$ & slip metric: primary-imposed minus secondary-imposed belt-line speed \\ \addlinespace @@ -1383,17 +1413,7 @@ \subsection{Dynamic and Inertial Modeling} (i.e., no internal dissipation). Although belt slip is explicitly modeled and affects torque capacity and shift dynamics, -the associated power loss $P_{\text{loss}}$ is neglected. -\end{assumptionblock} - -\begin{assumptionblock}{assump:single_slip_interface}{Single Active Slip Interface} -At any instant, the belt is assumed to either adhere to both pulleys -or slip at exactly one pulley contact. -Simultaneous slip at both pulley contacts is not modeled. - -If operating conditions would otherwise cause both contacts to exceed -their traction limits, the model assumes an instantaneous transition -between single-slip states. +the associated power loss is neglected. \end{assumptionblock} % ====================================================== @@ -1699,7 +1719,7 @@ \subsection{State Vector} \omega_s \\ s \\ \dot{s} \\ -\hat{v}_b +v_b \end{bmatrix} } @@ -1718,8 +1738,7 @@ \subsection{State Vector} \item $\dot{s}$ is the axial shift velocity (\si{\meter\per\second}). - \item $\hat{v}_b$ is the modeled belt transport speed - (\si{\meter\per\second}), represented as a closure-oriented dynamic state. + \item $v_b$ is the belt transport speed (\si{\meter\per\second}). \end{itemize} % -------------------------------------------------------------- @@ -1743,14 +1762,11 @@ \subsection{Independent Dynamic Degrees of Freedom} Because axial motion follows second-order dynamics, both the axial position $s$ and its rate $\dot{s}$ are required to fully describe its evolution. -The quantity $\hat{v}_b$ can be viewed as an additional belt-related degree of -freedom in a fully resolved belt model. In the present reduced-order -formulation, however, detailed belt internal mechanics are not represented -explicitly. Instead, $\hat{v}_b$ is introduced as an effective dynamic closure -variable governed by compatibility and closure relations introduced in later -sections. Thus, -$\hat{v}_b$ acts as an approximated surrogate for belt transport dynamics, -rather than a fully resolved independent mechanical coordinate. +The quantity $v_b$ is treated as an independent belt-transport degree of +freedom. While detailed internal +belt mechanics remain unresolved, $v_b$ is modeled as a dynamic state whose +evolution is given by an explicit equation of motion derived from torque, +traction, and compatibility relations at the pulley–belt interfaces. % -------------------------------------------------------------- \subsection{Time Integration Perspective} @@ -1767,14 +1783,13 @@ \subsection{Time Integration Perspective} \item The axial velocity $\dot{s}$ is updated according to the axial acceleration. - \item The modeled belt transport speed $\hat{v}_b$ is updated according - to its closure law. + \item The belt transport speed $v_b$ is updated according to its angular acceleration. \end{itemize} The angular and axial accelerations are determined from the governing equations derived in subsequent sections. Once the state vector has been updated, all other quantities in the model are computed directly from the -current values of $\omega_p$, $\omega_s$, $s$, $\dot{s}$, and $\hat{v}_b$. +current values of $\omega_p$, $\omega_s$, $s$, $\dot{s}$, and $v_b$. \section{Pulley Geometry}\label{sec:geometry} @@ -3315,36 +3330,43 @@ \subsection{Ramp Geometry Parameterization}\label{sec:ramp-geometry} \subsection{Primary Axial Force Model}\label{sec:primary-axial} -The primary axial clamping force is generated by the interaction -between centrifugal loading of the flyweights and the restoring -force of the primary compression spring. +The primary axial clamping force arises from the competition between +centrifugal loading of the flyweights and the restoring force of the +primary compression spring. -As shown in \Fig{fig:primary_cvt_3d}, each flyweight of mass $m_f$ -rotates with angular speed $\omega_p$ and experiences an outward -radial centrifugal force. This radial force acts along the ramp -profile $r_f(s)$ and is redirected into an axial closing force -through the ramp geometry described in \Sec{sec:ramp-geometry}. +As shown in \Fig{fig:primary_axial_mechanism}, each flyweight of mass +$m_f$ rotates with angular speed $\omega_p$ at an instantaneous radius +$r_{f,0}+r_f(s)$, where $r_{f,0}$ is the flyweight radius in the +reference configuration $s=0$ and $r_f(s)$ is the additional radial +displacement induced by the ramp. The resulting outward centrifugal +force is redirected by the ramp geometry described in +\Sec{sec:ramp-geometry} into an axial closing force on the movable +primary sheave. Opposing this motion is the primary compression spring, characterized by stiffness $k_p$ and preload $x_{p,0}$. As the sheaves close -($s$ increases), the spring compresses and produces a restoring -axial force resisting further closure. +($s$ increases), the spring compression increases, producing a larger +restoring force that resists further closure. -The net axial force on the movable primary sheave is therefore -the axial component of the ramp-redirected centrifugal force -minus the spring force. This net force determines the axial -acceleration $\ddot{s}$ of the primary assembly. +The resulting primary axial force is therefore the ramp-redirected +centrifugal contribution minus the spring force. This quantity, +denoted $F_{p,ax}(s,\omega_p)$, provides the primary-side contribution +to the generalized axial force used in the shift equation of motion. \begin{figure}[H] \centering -\includegraphics[width=0.7\linewidth]{./illustrations/pulleyForces/primary_cvt_mechanisms.png} -\caption{Primary CVT assembly showing flyweight mass $m_f$, ramp profile $r_f(s)$, -compression spring with stiffness $k_p$ and preload $x_{p,0}$, and axial shift -coordinate $s$.} -\label{fig:primary_cvt_3d} +\includegraphics[width=\linewidth]{./illustrations/pulleyForces/primary_cvt_mechanisms.png} +\caption{Primary CVT axial-force mechanism. Each flyweight of mass $m_f$ +rotates at angular speed $\omega_p$ and experiences an outward +centrifugal force at total radius $r_{f,0}+r_f(s)$. The ramp geometry +redirects this radial loading into an axial closing force on the +movable primary sheave in the $+s$ direction, while the primary +compression spring $(k_p,\;x_{p,0})$ provides an opposing restoring +force. The lower flyweight--ramp pair is symmetric and shown unlabeled +for clarity.} +\label{fig:primary_axial_mechanism} \end{figure} -The state variable governing primary rotation is $\omega_p$. \paragraph{Centrifugal Loading of Flyweights} From \theoryRef{th:centrifugal_force}, @@ -3898,14 +3920,14 @@ \subsection{Belt Centrifugal Force on the Wrap} \paragraph{3. Angular Velocity of the Belt Mass} The rotating belt mass on pulley \(j\) must also be assigned an angular speed. -From \Sec{sec:state}, \(\hat{v}_b\) denotes the modeled belt transport speed. +From \Sec{sec:state}, \(v_b\) denotes the belt transport speed. The corresponding angular velocity of the belt mass centroid on pulley \(j\) is then \begin{equation} \omega_{b,j} = -\frac{\hat{v}_b}{r_{j,\mathrm{cm}}(s)}. +\frac{v_b}{r_{j,\mathrm{cm}}(s)}. \label{eq:omega_bj_definition} \end{equation} @@ -3926,11 +3948,11 @@ \subsection{Belt Centrifugal Force on the Wrap} \nonumber\\[6pt] &= \left(\rho_b A_b\,r_{j,\mathrm{cm}}\,\dd\theta\right) -\left(\frac{\hat{v}_b}{r_{j,\mathrm{cm}}}\right)^2 +\left(\frac{v_b}{r_{j,\mathrm{cm}}}\right)^2 r_{j,\mathrm{cm}} \nonumber\\[6pt] &= -\rho_b A_b\,\hat{v}_b^2\,\dd\theta. +\rho_b A_b\,v_b^2\,\dd\theta. \label{eq:dFc_belt_j} \end{align} @@ -3946,14 +3968,14 @@ \subsection{Belt Centrifugal Force on the Wrap} F_{c,j,\mathrm{rad}} &= \int_{-\phi_j/2}^{\phi_j/2} -\rho_b A_b\,\hat{v}_b^2\,\dd\theta +\rho_b A_b\,v_b^2\,\dd\theta \nonumber\\[6pt] &= -\rho_b A_b\,\hat{v}_b^2 +\rho_b A_b\,v_b^2 \int_{-\phi_j/2}^{\phi_j/2}\dd\theta \nonumber\\[6pt] &= -\rho_b A_b\,\hat{v}_b^2\,\phi_j(s). +\rho_b A_b\,v_b^2\,\phi_j(s). \label{eq:Fc_rad_wrap_j} \end{align} @@ -3997,11 +4019,12 @@ \subsection{Belt Centrifugal Force on the Wrap} Canceling \(\dd s\) yields -\[ +\begin{equation} F_{\mathrm{ax}} = \frac{F_{\mathrm{rad}}}{2\tan\beta}. -\] +\label{eq:radial_to_axial_conversion} +\end{equation} Thus, the axial clamping contribution from belt centrifugal force on pulley \(j\) is @@ -4009,7 +4032,7 @@ \subsection{Belt Centrifugal Force on the Wrap} F_{c,j} = \frac{ -\rho_b A_b\,\hat{v}_b^2\,\phi_j(s) +\rho_b A_b\,v_b^2\,\phi_j(s) }{ 2\tan\beta }. @@ -4122,8 +4145,8 @@ \subsection{Axial Shift Dynamics}\label{sec:shift-dyn} \begin{itemize} \item $F_{p,\mathrm{ax}}(s,\omega_p)$ from \Eq{eq:primary_axial}, \item $F_{s,\mathrm{ax}}(s,\tau_s)$ from \Eq{eq:secondary_axial}, - \item $F_{c,p}(s,\hat{v}_b)$ from \Eq{eq:Fcj_general} with $j=p$, - \item $F_{c,s}(s,\hat{v}_b)$ from \Eq{eq:Fcj_general} with $j=s$. + \item $F_{c,p}(s,v_b)$ from \Eq{eq:Fcj_general} with $j=p$, + \item $F_{c,s}(s,v_b)$ from \Eq{eq:Fcj_general} with $j=s$. \end{itemize} With the sign convention defined above, the generalized force @@ -4199,658 +4222,126 @@ \subsection{Section Summary and Forward Outlook}\label{sec:axial-summary} attention turns to the torque transmission mechanism that closes the remaining dynamical loop. +\section{No-Slip Belt Dynamics and Torque Demand} +\label{sec:no-slip-belt-dynamics} -% ============================================================== -\section{Belt Motion, Torque Transmission, and Contact States} -\label{sec:belt_motion_torque_transmission_contact_states} - -The axial force model derived in \Sec{sec:axial} depends on quantities -that are not determined by axial motion alone. These remaining quantities -enter through the belt, which couples the primary and secondary pulleys in -two distinct ways. First, the belt moves through the pulley system with a -transport speed that enters the belt centrifugal loading terms. Second, -through frictional contact with the pulley faces, it transmits tangential -force and therefore torque between the pulleys. The purpose of this section -is to introduce the belt as the coupling medium of the CVT, define the -belt transport motion and torque path through the system, and then -distinguish the stick and slip states that govern transmission behavior. - -% ============================================================== -\subsection{The Belt as the Coupling Medium} - -The primary and secondary pulleys do not interact directly. -All mechanical interaction between them occurs through the belt. -The belt is therefore the coupling medium of the CVT. - -This coupling occurs in two distinct ways. - -First, the belt moves through the pulley system with a transport speed. -Because the belt has mass, this motion contributes to the dynamics through -belt-speed-dependent effects such as centrifugal loading. - -Second, the belt transmits tangential traction through frictional contact -with the pulley faces. This tangential interaction is what allows torque -to pass from the primary to the secondary. - -These two roles are illustrated in \Fig{fig:torque_path_1}. The primary pulley -drives the belt, the belt moves through the transmission path, and the belt in -turn drives the secondary pulley. - -\begin{figure}[H] - \centering - \includegraphics[width=0.8\linewidth]{./illustrations/torque/cvt_overview_forces.png} - \caption{Belt-mediated coupling in the CVT. The primary pulley applies - torque to the belt, the belt moves through the transmission path, and the - belt applies torque to the secondary pulley. Through frictional contact, - the belt also transmits tangential force between the pulley faces.} - \label{fig:torque_path_1} -\end{figure} - -To describe this interaction, the following belt-mediated quantities will be used: - -\begin{align} -\tau_p &:\ \text{torque applied by the primary pulley onto the belt}, \\ -\tau_s &:\ \text{torque applied by the belt onto the secondary pulley}, \\ -F_t &:\ \text{effective tangential force transmitted through the belt}, \\ -\hat{v}_b &:\ \text{modeled belt transport speed}. -\end{align} - -The quantities $\tau_p$ and $\tau_s$ describe how torque enters and leaves the -belt transmission path. -The quantity $F_t$ describes the tangential traction carried through the belt. -The quantity $\hat{v}_b$ describes the motion of the belt itself through the -pulley system. - -The remainder of this section develops these belt-mediated quantities in order. -We first examine the transport motion of the belt, then the tangential force -and torque path carried through it, and finally the contact states that govern -whether the transmission is in stick or slip. - -% ============================================================== -\subsection{Belt Transport Motion}\label{sec:belt_transport_motion} - -Before torque transmission can be described, the motion of the belt itself -must be established. - -Under \AssRef{assump:constant_belt_length}, the belt is assumed to remain -taut and its total length is fixed. As a result, the belt does not admit -independent local stretching or compression along different portions of its -path. Instead, it moves through the CVT as a single continuous body with one -transport speed at any given instant. - -To represent this motion in the dynamic model, let - -\[ -\hat{v}_b(t) -\] - -denote the modeled belt transport speed. This quantity describes the rate at -which belt material moves along the transmission path and is the belt-speed -state introduced in \Sec{sec:state}. - -If a belt segment is adhering locally to pulley \(j \in \{p,s\}\), then the -belt speed at that contact must match the circumferential speed of the pulley -at the radius where the belt mass is modeled to move. Using the centroid -radius from \Eq{eq:rj_cm_definition}, the pulley-imposed belt-line speeds are therefore - -\begin{equation} -v_{p,\mathrm{cm}} -= -r_{p,\mathrm{cm}}(s)\,\omega_p, -\qquad -v_{s,\mathrm{cm}} -= -r_{s,\mathrm{cm}}(s)\,\omega_s. -\label{eq:belt_line_speeds} -\end{equation} - -These quantities represent the belt transport speeds that would be imposed by -the primary and secondary pulleys, respectively, if the belt were adhering -locally at each contact. - -If both pulley contacts are adhering simultaneously, then the belt cannot move -at two different speeds at once. The transport speed must therefore satisfy the -compatibility condition - -\begin{equation} -\hat{v}_b -= -v_{p,\mathrm{cm}} -= -v_{s,\mathrm{cm}}. -\label{eq:vb_stick_compatibility} -\end{equation} - -This is the kinematic condition associated with sticking contact. - -If the two pulley-imposed belt-line speeds are unequal, then simultaneous -adherence at both contacts is not kinematically possible. In that case, the -transmission must depart from the sticking condition and enter a slipping -state, which is discussed later in this section. - -With the belt transport motion now identified, the next step is to determine -how tangential force and power are carried through this moving belt. - - -% ============================================================== -\subsection{Belt Power Flow and Torque Transmission} - -With the belt transport motion now identified, the next step is to determine -how force and power are carried through the moving belt. - -Since the belt is the only mechanical connection between the pulleys, it must -carry the power transmitted through the CVT. - -Torque transmission arises from tangential forces acting between the belt and -the pulley faces. Let \(F_t\) denote the effective tangential force transmitted -through the belt. - -The mechanical power carried by the belt is therefore - -\begin{equation} -P = F_t \,\hat{v}_b. -\label{eq:belt_power} -\end{equation} - -Under \AssRef{assump:ideal_cvt_power}, this power is conserved through the -transmission, so that the power entering the belt at the primary equals the -power leaving at the secondary. - -The same transmitted tangential force \(F_t\) acts at the effective contact -radius of each pulley. The torques associated with this force are therefore - -\begin{align} -\tau_p &= r_{p,\mathrm{eff}} \, F_t, \\ -\tau_s &= r_{s,\mathrm{eff}} \, F_t. -\label{eq:torques_from_Ft} -\end{align} - -Here, \(r_{p,\mathrm{eff}}\) and \(r_{s,\mathrm{eff}}\) are the effective -contact radii defined by \Eq{eq:primary_effective_radius} and -\Eq{eq:secondary_effective_radius}, respectively. These radii determine the torque produced -by the transmitted tangential force. -Since both torques arise from the same transmitted force, \(F_t\) can be -eliminated directly. From the primary expression, - -\begin{equation} -F_t = \frac{\tau_p}{r_{p,\mathrm{eff}}}. -\label{eq:Ft_from_taup} -\end{equation} - -Substituting this into the secondary torque relation gives - -\begin{equation} -\tau_s -= -r_{s,\mathrm{eff}} -\left( -\frac{\tau_p}{r_{p,\mathrm{eff}}} -\right) -= -\frac{r_{s,\mathrm{eff}}}{r_{p,\mathrm{eff}}}\,\tau_p. -\label{eq:taus_from_taup_intermediate} -\end{equation} - -Recalling the geometric definition of the CVT ratio from \Eq{eq:cvt_ratio}, - -\begin{equation} -R = \frac{r_{s,\mathrm{eff}}}{r_{p,\mathrm{eff}}}, -\label{eq:R_eff_repeat} -\end{equation} - -it follows that - -\begin{equation} -\tau_s = R\,\tau_p. -\label{eq:torque_ratio} -\end{equation} - -Thus, the torque carried through the belt scales directly with the current -CVT ratio. The primary and secondary torques are not independent quantities, -but two descriptions of the same belt-mediated transmission process. - -This observation motivates the introduction of a single \emph{coupling torque}. - -\begin{equation} -\tau_c \equiv \tau_p. -\end{equation} - -The corresponding secondary torque is then -\begin{equation} -\tau_s = R \, \tau_c. -\end{equation} - -The transmitted torque is thus not a property of either pulley in isolation, but -of the belt--pulley interaction as a whole. The quantities $\tau_p$ and $\tau_s$ -represent the primary-side and secondary-side expressions of this same coupling -torque. - - -\subsection{Transmission Regimes} -\label{sec:transmission_regimes} - -The coupling torque $\tau_c$ defined in \Eq{eq:torque_ratio} is the torque -actually transmitted through the belt. -It is not prescribed externally. -Instead, its value depends on the contact state of the transmission. - -In \Sec{sec:belt_transport_motion}, the belt was introduced as a moving body -with modeled transport speed $\hat{v}_b$. -That quantity describes the motion of belt material through the CVT and is used -in belt-speed-dependent effects such as centrifugal loading. -The present subsection addresses a different issue: whether the belt is locally -adhering or slipping at the pulley contacts. -For this purpose, the relevant kinematic quantities are the tangential contact -speeds imposed by the pulley surfaces. - -\paragraph{No-slip torque.} - -If the belt remains adhered at the pulley contacts, then the transmission -behaves as a sticking constraint. -In that case, the transmitted torque is whatever value is required to preserve -that adhered motion. - -We denote this required torque by -\begin{equation} -\tau_{\mathrm{ns}}, -\end{equation} -where the subscript ``ns'' denotes \emph{no slip}. -Thus, in a sticking state, -\begin{equation} -\tau_c = \tau_{\mathrm{ns}}. -\end{equation} - -\paragraph{Traction limits.} - -Adherence cannot be maintained for arbitrary transmitted torque. -The belt--pulley contact can only sustain a finite tangential traction, and -therefore only a finite coupling torque. - -We therefore introduce directional traction bounds and write the admissible -sticking interval as -\begin{equation} -\tau_- \le \tau_{\mathrm{ns}} \le \tau_+. -\label{eq:stick_admissible_interval} -\end{equation} - -If \Eq{eq:stick_admissible_interval} holds, then the no-slip torque lies within -the available traction limits, so sticking is physically admissible. - -If instead $\tau_{\mathrm{ns}}$ lies outside this interval, then the torque -required to maintain adherence cannot be supported by the contact, and slip must -occur. - -\paragraph{Why a slip metric is still needed.} - -The traction condition alone is not enough to determine the regime. -Once slipping has begun, the transmission does not return immediately to stick -simply because $\tau_{\mathrm{ns}}$ later re-enters the admissible interval. -Stick is only recovered once the existing relative motion has decayed to zero. - -For this reason, one further quantity is needed: a measure of the relative -contact-speed mismatch. - -In analogy with the pulley-imposed belt-line speeds introduced in -\Sec{sec:belt_transport_motion}, define the pulley-imposed tangential contact -speeds at the effective radii by -\begin{equation} -u_p = r_{p,\mathrm{eff}}\omega_p, -\qquad -u_s = r_{s,\mathrm{eff}}\omega_s. -\label{eq:contact_surface_speeds} -\end{equation} - -These are the tangential speeds that the primary and secondary pulley surfaces -would impose at their effective contact radii if each were enforcing local -adherence. - -Under \AssRef{assump:single_slip_interface}, the model permits slip at only one -pulley contact at a time. -A single scalar mismatch is therefore sufficient: -\begin{equation} -v_{\Delta} -= -u_p - u_s -= -r_{p,\mathrm{eff}}\omega_p -- -r_{s,\mathrm{eff}}\omega_s. -\label{eq:v_delta_def} -\end{equation} - -If -\begin{equation} -v_{\Delta}=0, -\label{eq:v_delta_zero} -\end{equation} -then the two pulley motions are compatible with a common adhered contact motion, -so a sticking state is kinematically possible. - -If instead -\begin{equation} -v_{\Delta}\neq 0, -\label{eq:v_delta_nonzero} -\end{equation} -then the two pulley contacts are demanding different tangential contact speeds, -and simultaneous adherence at both contacts is not possible. - -The sign of $v_{\Delta}$ indicates the slip direction: -if $v_{\Delta}>0$, the primary tends to drive the belt faster than the secondary -is accepting it; if $v_{\Delta}<0$, the opposite is true. - -\paragraph{Stick and slip states.} - -The transmission can therefore be in stick only when two conditions are -satisfied simultaneously: - -\begin{enumerate} - \item the required no-slip torque lies within the admissible traction - interval, and - \item the relative slip velocity has vanished. -\end{enumerate} - -This gives the regime structure -\begin{equation} -\tau_c = -\begin{cases} -\tau_{\mathrm{ns}}, -& -v_{\Delta}=0 -\;\text{and}\; -\tau_- \le \tau_{\mathrm{ns}} \le \tau_+, -\\[10pt] -\text{traction-limited value}, -& -\text{otherwise}. -\end{cases} -\label{eq:tau_c_regime_structure} -\end{equation} - -\paragraph{Physical interpretation.} - -If the required no-slip torque leaves the admissible interval, the contact can -no longer maintain adherence and slip begins. - -Once slip is present, the transmission remains on a slip branch until the -existing mismatch has been driven back to zero. -This is why the slip metric is needed in addition to the traction limits: -the traction limits determine whether sticking is admissible, while -$v_{\Delta}$ determines whether slipping is still occurring and, if so, in which -direction. Importantly, this does not determine at \emph{which} contact slip occurs, -only which is overruning the other. - - -% ============================================================== -\subsection{Regularized Belt-Speed Evolution} -\label{sec:belt_speed_evolution} - -The modeled belt transport speed $\hat{v}_b$ introduced in -\Sec{sec:belt_transport_motion} enters the axial force model through the -belt centrifugal terms derived in \Sec{sec:belt-centrifugal}. -Its evolution must therefore be specified as part of the complete dynamic model. - -\paragraph{Exact closure in stick.} - -From \Eq{eq:belt_line_speeds}, the pulley-imposed belt-line speeds are - -\[ -v_{p,\mathrm{cm}} = r_{p,\mathrm{cm}}(s)\,\omega_p, -\qquad -v_{s,\mathrm{cm}} = r_{s,\mathrm{cm}}(s)\,\omega_s. -\] - -When the transmission is in a sticking state, these two speeds are compatible. -In that case, the belt transport speed is determined exactly by the adhered -kinematics, so that - -\begin{equation} -\hat{v}_b = v_{p,\mathrm{cm}} = v_{s,\mathrm{cm}}. -\label{eq:vb_stick_exact} -\end{equation} - -In the same way, the time derivative of the belt transport speed is also -determined exactly in stick by differentiating the compatibility relation. -Using the primary-side expression gives - -\begin{equation} -\dot{\hat{v}}_b -= -\frac{\dd}{\dd t}\!\left(r_{p,\mathrm{cm}}\,\omega_p\right) -= -\dot r_{p,\mathrm{cm}}\,\omega_p -+ -r_{p,\mathrm{cm}}\,\dot\omega_p, -\label{eq:vb_dot_stick_primary} -\end{equation} - -and, equivalently, using the secondary-side expression gives - -\begin{equation} -\dot{\hat{v}}_b -= -\frac{\dd}{\dd t}\!\left(r_{s,\mathrm{cm}}\,\omega_s\right) -= -\dot r_{s,\mathrm{cm}}\,\omega_s -+ -r_{s,\mathrm{cm}}\,\dot\omega_s. -\label{eq:vb_dot_stick_secondary} -\end{equation} - -Thus, no additional approximation is required while the contacts remain stuck. - -\paragraph{Need for a slip closure.} - -When the transmission is slipping, the two pulley-imposed belt-line speeds no -longer agree. -At that point, the exact belt transport speed is no longer determined by -kinematic compatibility alone. -A fully resolved treatment would require a simultaneous branch-dependent -solution for contact state, transmitted torque, and adhered belt motion. - -Such a closure is beyond the level of detail adopted in the present model. -Instead, the belt transport speed is represented during slip by a regularized -dynamic approximation. -This preserves a continuous belt-speed state for use in belt-speed-dependent -terms, while avoiding a substantially more complicated contact solve. - -\paragraph{Regularized target speed.} - -To construct this approximation, define the target belt speed - -\begin{equation} -v_b^\ast -= -\frac{1}{2} -\left( -v_{p,\mathrm{cm}} + v_{s,\mathrm{cm}} -\right). -\label{eq:vb_target} -\end{equation} - -This quantity represents the midpoint of the two pulley-imposed belt-line -speeds and therefore provides a natural reference speed during slip. - -\paragraph{Evolution law.} - -Outside the sticking regime, the modeled belt speed is evolved toward -\(v_b^\ast\) using the first-order relaxation law - -\begin{equation} -\dot{\hat{v}}_b -= -\frac{v_b^\ast - \hat{v}_b}{T_b}, -\label{eq:vb_relaxation} -\end{equation} - -where \(T_b > 0\) is a belt-speed regularization time constant. - -A small value of \(T_b\) causes \(\hat{v}_b\) to track the target speed -rapidly, while a larger value produces a more gradual response. -In either case, \(\hat{v}_b\) remains continuous through transitions between -stick and slip. - -\paragraph{Resulting closure.} - -The modeled belt transport speed is therefore obtained from the regime-dependent -rule - -\begin{equation} -\hat{v}_b = -\begin{cases} -v_{p,\mathrm{cm}} = v_{s,\mathrm{cm}}, -& -\text{sticking regime}, -\\[10pt] -\text{solution of } -\dot{\hat{v}}_b = \dfrac{v_b^\ast - \hat{v}_b}{T_b}, -& -\text{slipping regime.} -\end{cases} -\label{eq:vb_regime_law} -\end{equation} - -Its corresponding time derivative is - -\begin{equation} -\dot{\hat{v}}_b = -\begin{cases} -\dot r_{p,\mathrm{cm}}\,\omega_p + r_{p,\mathrm{cm}}\,\dot\omega_p -= -\dot r_{s,\mathrm{cm}}\,\omega_s + r_{s,\mathrm{cm}}\,\dot\omega_s, -& -\text{sticking regime}, -\\[10pt] -\dfrac{v_b^\ast - \hat{v}_b}{T_b}, -& -\text{slipping regime.} -\end{cases} -\label{eq:vb_dot_regime_law} -\end{equation} - -This closure is exact in stick and regularized in slip. -Its purpose is not to resolve the detailed local adhered contact during slip, -but to provide a consistent and continuous modeled belt speed for the -belt-speed-dependent terms already present in the dynamic formulation. - -\subsection{Summary and Forward Outlook} - -This section established the belt-level quantities required to couple the -axial and rotational subsystems of the CVT model. +The axial force model developed in \Sec{sec:axial} determines how the shift +coordinate evolves once the primary and secondary axial forces are known. +However, the secondary axial force depends on the transmitted secondary torque +\(\tau_s\), which is not determined by the axial force balance itself. -The belt was introduced as the mechanical connection between the pulleys, -with two distinct dynamical roles: it transports belt mass through the -system and it transmits tangential force and therefore torque between the -pulley faces. On this basis, the modeled belt transport speed $\hat{v}_b$ -and the coupling torque $\tau_c$ were defined, and the stick and slip -regimes governing transmission behavior were identified. A regularized -evolution law for $\hat{v}_b$ was then introduced to close the -belt-speed-dependent terms of the model during slip while preserving exact -kinematic compatibility in stick. +This section develops the rotational and belt-dynamic relations needed to +determine that torque in the no-slip case. The primary pulley, belt, and +secondary pulley are treated as three coupled bodies: torque enters through the +primary, is carried through belt contact, and exits through the secondary into +the downstream drivetrain. -At this stage, the transmission structure has been established, but two -ingredients remain to be determined explicitly: the no-slip torque -$\tau_{\mathrm{ns}}$ and the traction limits $\tau_-$ and $\tau_+$. -The next section derives the no-slip torque required to maintain adhered -motion. The section following that then derives the admissible traction -bounds that determine when sticking can be sustained and when the -transmission must remain on a slip branch. +The goal is to derive the no-slip torque demand: the torque transfer that would +be required if the belt remained adhered to both pulleys. This result will later +be checked against the available belt--pulley traction limits to determine +whether no-slip motion is physically admissible. -\section{Derivation of the No-Slip Coupling Torque} -\label{sec:tau_ns} - -In \Sec{sec:transmission_regimes}, the coupling torque $\tau_c$ was introduced -as an internal torque whose value depends on the active transmission regime. -The purpose of this section is to derive the explicit form of $\tau_c$ on the -sticking branch, i.e.\ the no-slip torque $\tau_{\mathrm{ns}}$. - -We proceed in four steps: - -\begin{enumerate} - \item Write the rotational equations of motion for the primary and secondary pulleys. - \item Recall the external torque and inertia models on each side. - \item Impose the no-slip kinematic constraint. - \item Eliminate angular accelerations to obtain the no-slip coupling torque. -\end{enumerate} - -The result is the sticking-branch torque law used later in the regime model. - -% -------------------------------------------------------------- \subsection{System Definition and Torque Path} +\label{sec:belt-torque-path} \begin{figure}[H] \centering -\includegraphics[width=0.85\linewidth]{./illustrations/torque/cvt_overview_forces.png} -\caption{Torque transmission schematic. -Engine torque $\tau_{\text{eng}}$ drives the primary pulley -(angular velocity $\omega_p$). -Coupling torque $\tau_c$ is transmitted through the belt to the secondary pulley -(angular velocity $\omega_s$). -The secondary delivers torque to the downstream drivetrain, -which resists motion with torque $\tau_{\text{load}}$.} +\includegraphics[width=1.1\linewidth]{./illustrations/torque/cvt_overview_forces.png} +\caption{Torque path through the CVT. Engine torque +$\tau_{\mathrm{eng}}$ drives the primary pulley with angular speed +$\omega_p$. The primary pulley exchanges torque with the belt, the belt +exchanges torque with the secondary pulley, and the secondary pulley drives the +downstream drivetrain while opposing the reflected load torque +$\tau_{\mathrm{load}}$.} \label{fig:torque_path} \end{figure} -We model two rotating bodies: +The torque-transfer subsystem shown in \Fig{fig:torque_path} is modeled using +three bodies: \begin{itemize} - \item the primary side, rigidly connected to the engine, - \item the secondary side, driving the downstream drivetrain and vehicle. + \item the primary pulley, + \item the belt, + \item the secondary pulley. \end{itemize} -The external torques and effective inertias used below are +The primary pulley receives the engine torque $\tau_{\mathrm{eng}}$ and +exchanges torque with the belt. Let $\tau_p(t)$ denote the torque exerted by the +belt contact on the primary pulley. With the sign convention from +\Sec{sec:rotational_sign}, this torque opposes positive engine-driven rotation +of the primary pulley. -\begin{itemize} - \item $\tau_{\text{eng}}(t)$ : engine output torque applied to the primary pulley, - \item $\tau_{\text{load}}(t)$ : resisting torque reflected to the secondary pulley, - \item $I_p$ : total rotational inertia of the engine + primary CVT pulley, - \item $I_s$ : total effective rotational inertia about the secondary pulley. -\end{itemize} +The secondary pulley receives torque from the belt and delivers torque to the +downstream drivetrain. Let $\tau_s(t)$ denote the torque exerted by the belt +contact on the secondary pulley. This torque acts in the positive rotational +direction of the secondary pulley, while the reflected load torque +$\tau_{\mathrm{load}}(t)$ opposes the motion. -Their explicit models are recalled in the following subsections. -The remaining internal unknown is the transmitted coupling torque $\tau_c(t)$. +Applying \theoryRef{th:newton-rot} to the primary pulley gives -% -------------------------------------------------------------- -\subsection{Rotational Equations of Motion} +\bigeq{eq:primary_rotational_eom_belt}{Primary Pulley Rotational Dynamics}{ +\tau_{\mathrm{eng}}(t) - \tau_p(t) += +I_p \dot{\omega}_p(t) +} -Applying \theoryRef{th:newton-rot} to the primary side gives +Similarly, applying \theoryRef{th:newton-rot} to the secondary pulley gives -\bigeq{eq:primary_rot_dyn}{Primary Pulley Rotational Dynamics}{ -\tau_{\text{eng}}(t) - \tau_c(t) +\bigeq{eq:secondary_rotational_eom_belt}{Secondary Pulley Rotational Dynamics}{ +\tau_s(t) - \tau_{\mathrm{load}}(t) = -I_p\,\dot{\omega}_p(t) +I_s \dot{\omega}_s(t) } -Using the secondary-side no-slip torque mapping established in -\Eq{eq:torque_ratio}, the secondary rotational equation on the sticking branch is +The belt is described by the transport speed $v_b(t)$ introduced in +\Sec{sec:state}. Under \AssRef{assump:constant_belt_length}, the belt moves as a +continuous path with a single belt-line speed. Its angular speed around each +pulley may differ because angular speed depends on local radius, but the +transport speed $v_b$ is common to the belt motion. + +For this reason, the belt equation of motion is written along the belt path +rather than about a pulley axis. A contact torque produces an equivalent +belt-line force when divided by the effective torque radius. Using the effective +radii defined in \Sec{sec:effective-radius-placeholder}, the primary contact +contributes $\tau_p/r_{p,\mathrm{eff}}$ to the belt-line motion, while the +secondary contact contributes $\tau_s/r_{s,\mathrm{eff}}$ in the opposing +direction. Applying \theoryRef{th:newton-trans} to the belt therefore gives -\bigeq{eq:secondary_rot_dyn}{Secondary Pulley Rotational Dynamics}{ -R(t)\tau_c(t) - \tau_{\text{load}}(t) +\bigeq{eq:belt_transport_eom}{Belt Transport Dynamics}{ +m_b \dot v_b(t) = -I_s\,\dot{\omega}_s(t) +\frac{\tau_p(t)}{r_{p,\mathrm{eff}}(s)} +- +\frac{\tau_s(t)}{r_{s,\mathrm{eff}}(s)} } -At this point, \Eq{eq:primary_rot_dyn} and \Eq{eq:secondary_rot_dyn} -provide two equations for the three internal unknowns +Here $m_b$ is the total belt mass defined in \Eq{eq:mb_def}. Together, +\Eq{eq:primary_rotational_eom_belt}, \Eq{eq:secondary_rotational_eom_belt}, and +\Eq{eq:belt_transport_eom} define the equations of motion for the three bodies +in the torque path. + +The quantities appearing in these equations fall into two groups. The first +group consists of externally supplied torques and inertial parameters: \[ -\dot{\omega}_p(t),\qquad -\dot{\omega}_s(t),\qquad -\tau_c(t). +\tau_{\mathrm{eng}}, \qquad \tau_{\mathrm{load}}, \qquad I_p, \qquad I_s, +\qquad m_b. \] +These are not determined by the belt contact state itself. They are defined by +the engine model, the reflected vehicle load model, and the lumped inertia and +belt-mass definitions recalled in the following subsection. -The remaining quantities appearing in these equations ---- namely the applied torques $\tau_{\text{eng}}(t)$ and $\tau_{\text{load}}(t)$, -the effective inertias $I_p$ and $I_s$, and the geometric ratio $R(t)$ --- -are treated as known inputs, parameters, or previously defined geometric -quantities. -Their explicit forms are recalled in the following subsections. - -Thus, the rotational system is not yet closed: -two equations are available for three unknowns. -A third relation is therefore required to determine the coupling torque. -For the no-slip branch, that final relation will come from the sticking -kinematic constraint introduced after the torque and inertia models are stated. +The second group consists of the torque-transfer unknowns: +\[ +\dot{\omega}_p, \qquad \dot{\omega}_s, \qquad \dot v_b, \qquad \tau_p, +\qquad \tau_s. +\] +These five quantities describe the accelerations of the three bodies and the two +contact torques exchanged through the belt. The three equations of motion above +are therefore not sufficient to determine them on their own. The remaining +closure comes from the belt--pulley contact state, beginning with the no-slip +compatibility condition derived after the external torque and inertia models are +recalled. -% -------------------------------------------------------------- \subsection{Engine Torque Model} \label{sec:tau_eng_model} @@ -4890,8 +4381,7 @@ \subsection{Engine Torque Model} No specific functional form is required. The dynamical model only requires the mapping -$\omega_p \mapsto \tau_{\text{eng}}$ -to evaluate \Eq{eq:primary_rot_dyn}. +$\omega_p \mapsto \tau_{\text{eng}}$. % -------------------------------------------------------------- \paragraph{Example: Kohler CH440 Engine Curve} @@ -4919,7 +4409,7 @@ \subsection{Load Torque Model} \label{sec:tau_load_model} The resisting torque $\tau_{\text{load}}(t)$ in -\Eq{eq:secondary_rot_dyn} represents the +\Eq{eq:secondary_rotational_eom_belt} represents the longitudinal road load reflected to the secondary pulley. We proceed in three stages: @@ -5113,7 +4603,7 @@ \subsection{Load Torque Model} % -------------------------------------------------------------- \subsection{Inertia Definitions} -The rotational equations \Eq{eq:primary_rot_dyn}--\Eq{eq:secondary_rot_dyn} +The rotational equations \Eq{eq:primary_rotational_eom_belt}--\Eq{eq:secondary_rotational_eom_belt} require the effective rotational inertia about each pulley axis. % -------------------------------------------------------------- @@ -5244,227 +4734,256 @@ \subsection{Inertia Definitions} opposing angular acceleration of the secondary pulley. % -------------------------------------------------------------- -\subsection{No-Slip Ratio Kinematics} -As established in \Sec{sec:transmission_regimes}, the sticking branch is -characterized by the condition -\[ -v_{\Delta}=0, -\] -where \(v_{\Delta}\) was defined in \Eq{eq:v_delta_def} as + +\subsection{No-Slip Kinematic Compatibility} +\label{sec:no-slip-compatibility} + +The equations of motion in \Sec{sec:belt-torque-path} describe the torque-transfer +subsystem before any contact constraint has been imposed. To form the no-slip +candidate, we now require the belt to adhere to each pulley contact. + +Let \[ -v_{\Delta} -= -r_{p,\text{eff}}(t)\,\omega_p(t) -- -r_{s,\text{eff}}(t)\,\omega_s(t). +j \in \{p,s\} \] - -Setting this equal to zero gives +denote either the primary or secondary pulley. If the belt is adhered to pulley +\(j\), then the belt transport speed must match the tangential surface speed of +that pulley at the effective torque radius. Therefore, \begin{equation} -r_{p,\text{eff}}(t)\,\omega_p(t) -= -r_{s,\text{eff}}(t)\,\omega_s(t). -\label{eq:no_slip_condition} +v_b(t) = r_{j,\mathrm{eff}}(s(t))\,\omega_j(t), +\qquad j \in \{p,s\}. +\label{eq:no_slip_identity_j} \end{equation} -Using the geometric CVT ratio +Equation \Eq{eq:no_slip_identity_j} is the velocity-level no-slip condition. Since +the equations of motion involve accelerations, we differentiate it with respect +to time. Starting from \[ -R(t) -= -\frac{r_{s,\text{eff}}(t)}{r_{p,\text{eff}}(t)}, +v_b(t) = r_{j,\mathrm{eff}}(s(t))\,\omega_j(t), \] -\Eq{eq:no_slip_condition} becomes +the product rule gives \begin{equation} -\omega_p(t)=R(t)\,\omega_s(t). -\label{eq:ratio_speed_constraint} +\dot v_b(t) += +\dot r_{j,\mathrm{eff}}(t)\,\omega_j(t) ++ +r_{j,\mathrm{eff}}(s(t))\,\dot\omega_j(t). +\label{eq:no_slip_vb_dot_j} \end{equation} -This is the defining kinematic relation of the sticking branch. -If \Eq{eq:ratio_speed_constraint} does not hold, the present derivation no -longer applies and the transmission must instead be described by the slip -branch. +Since the effective radius depends on time only through the shift coordinate +\(s(t)\), +\begin{equation} +\dot r_{j,\mathrm{eff}}(t) += +\frac{d r_{j,\mathrm{eff}}}{ds}\dot s(t). +\label{eq:reff_time_derivative_j} +\end{equation} -Differentiating \Eq{eq:ratio_speed_constraint} with respect to time gives +Solving Equation \Eq{eq:no_slip_vb_dot_j} for the pulley angular acceleration +gives \begin{equation} -\dot{\omega}_p(t) +\dot\omega_j(t) = -R(t)\,\dot{\omega}_s(t) -+ -\omega_s(t)\,\dot R(t), -\label{eq:alpha_relation} +\frac{ +\dot v_b(t)-\dot r_{j,\mathrm{eff}}(t)\,\omega_j(t) +}{ +r_{j,\mathrm{eff}}(s(t)) +}, +\qquad j \in \{p,s\}. +\label{eq:no_slip_omega_dot_j} \end{equation} -which is the corresponding acceleration-level compatibility condition. -\subsection{Eliminating Angular Accelerations} +Together with the three equations of motion from \Sec{sec:belt-torque-path}, +this acceleration-level compatibility supplies the no-slip closure. The next +step is to substitute \Eq{eq:no_slip_omega_dot_j} into the pulley +equations of motion and solve for the contact torque demands associated with the +no-slip candidate. -We now combine the primary rotational dynamics -\Eq{eq:primary_rot_dyn}, -the secondary rotational dynamics -\Eq{eq:secondary_rot_dyn}, -and the no-slip compatibility relation -\Eq{eq:alpha_relation} -to solve for the coupling torque on the sticking branch. -From \Eq{eq:primary_rot_dyn}, +\subsection{Solving the No-Slip Torque Demand} +\label{sec:no-slip-torque-demand} +The no-slip compatibility relation from \Sec{sec:no-slip-compatibility} can now +be combined with the three equations of motion from \Sec{sec:belt-torque-path}. +For compactness, define \[ -\tau_c +r_p = r_{p,\mathrm{eff}}(s), +\qquad +r_s = r_{s,\mathrm{eff}}(s), +\] +with corresponding time derivatives \(\dot r_p\) and \(\dot r_s\). + +On the no-slip candidate branch, Equation \Eq{eq:no_slip_omega_dot_j} gives +\[ +\dot\omega_p = -\tau_{\text{eng}} -- -I_p\,\dot{\omega}_p. +\frac{\dot v_{b,\mathrm{ns}}-\dot r_p\omega_p}{r_p}, +\qquad +\dot\omega_s += +\frac{\dot v_{b,\mathrm{ns}}-\dot r_s\omega_s}{r_s}. \] -Substituting \Eq{eq:alpha_relation} gives +Substituting these relations into the primary rotational equation +\Eq{eq:primary_rotational_eom_belt} gives the primary contact torque required +to maintain no slip: -\begin{equation} -\tau_c +\bigeq{eq:tau_p_ns}{Primary No-Slip Torque Demand}{ +\tau_{p,\mathrm{ns}} = -\tau_{\text{eng}} +\tau_{\mathrm{eng}} - -I_p\bigl(R\dot{\omega}_s+\omega_s\dot R\bigr). -\label{eq:tau_c_intermediate} -\end{equation} +I_p +\left( +\frac{\dot v_{b,\mathrm{ns}}-\dot r_p\omega_p}{r_p} +\right) +} -From \Eq{eq:secondary_rot_dyn}, +Similarly, substituting into the secondary rotational equation +\Eq{eq:secondary_rotational_eom_belt} gives the secondary contact torque required +to maintain no slip: + +\bigeq{eq:tau_s_ns}{Secondary No-Slip Torque Demand}{ +\tau_{s,\mathrm{ns}} += +\tau_{\mathrm{load}} ++ +I_s +\left( +\frac{\dot v_{b,\mathrm{ns}}-\dot r_s\omega_s}{r_s} +\right) +} +At this point, both torque demands are expressed in terms of the unknown no-slip +belt acceleration \(\dot v_{b,\mathrm{ns}}\). The belt equation of motion +\Eq{eq:belt_transport_eom} provides the remaining relation: \[ -\dot{\omega}_s +m_b \dot v_{b,\mathrm{ns}} = -\frac{R\tau_c-\tau_{\text{load}}}{I_s}. +\frac{\tau_{p,\mathrm{ns}}}{r_p} +- +\frac{\tau_{s,\mathrm{ns}}}{r_s}. \] -Substituting this into \Eq{eq:tau_c_intermediate} gives - -\begin{align} -\tau_c -&= -\tau_{\text{eng}} +Substituting \Eq{eq:tau_p_ns} and \Eq{eq:tau_s_ns} gives +\[ +m_b \dot v_{b,\mathrm{ns}} += +\frac{1}{r_p} +\left[ +\tau_{\mathrm{eng}} - I_p -\left[ -R\left( -\frac{R\tau_c-\tau_{\text{load}}}{I_s} +\left( +\frac{\dot v_{b,\mathrm{ns}}-\dot r_p\omega_p}{r_p} \right) -+ -\omega_s\dot R \right] -\nonumber\\[6pt] -&= -\tau_{\text{eng}} - -\frac{I_p}{I_s} +\frac{1}{r_s} +\left[ +\tau_{\mathrm{load}} ++ +I_s \left( -R^2\tau_c-R\tau_{\text{load}} +\frac{\dot v_{b,\mathrm{ns}}-\dot r_s\omega_s}{r_s} \right) -- -I_p\omega_s\dot R. -\end{align} - -Collecting terms yields +\right]. +\] -\begin{equation} -\tau_c +Expanding and collecting the terms containing \(\dot v_{b,\mathrm{ns}}\) gives +\[ \left( -1+\frac{I_p}{I_s}R^2 +m_b ++ +\frac{I_p}{r_p^2} ++ +\frac{I_s}{r_s^2} \right) +\dot v_{b,\mathrm{ns}} = -\tau_{\text{eng}} -+ -\frac{I_p}{I_s}R\tau_{\text{load}} +\frac{\tau_{\mathrm{eng}}}{r_p} - -I_p\omega_s\dot R. -\label{eq:tau_c_rearranged} -\end{equation} - -Because this derivation has imposed the sticking constraint -\Eq{eq:ratio_speed_constraint}, the resulting solution is precisely the no-slip -coupling torque. - -% -------------------------------------------------------------- -\subsection{Closed-Form No-Slip Coupling Torque} +\frac{\tau_{\mathrm{load}}}{r_s} ++ +\frac{I_p \dot r_p\omega_p}{r_p^2} ++ +\frac{I_s \dot r_s\omega_s}{r_s^2}. +\] -The no-slip coupling torque is therefore +Solving for the no-slip belt acceleration yields -\bigeq{eq:tau_ns_final}{No-Slip Coupling Torque}{ -\tau_{\mathrm{ns}}(t) +\bigeq{eq:no_slip_vb_dot}{No-Slip Belt Acceleration}{ +\dot v_{b,\mathrm{ns}} = \frac{ -\tau_{\text{eng}}(t) -+ -\dfrac{I_p}{I_s}R(t)\,\tau_{\text{load}}(t) +\dfrac{\tau_{\mathrm{eng}}}{r_p} - -I_p\,\omega_s(t)\,\dot R(t) +\dfrac{\tau_{\mathrm{load}}}{r_s} ++ +\dfrac{I_p \dot r_p\omega_p}{r_p^2} ++ +\dfrac{I_s \dot r_s\omega_s}{r_s^2} }{ -1+\dfrac{I_p}{I_s}R(t)^2 -}. +m_b ++ +\dfrac{I_p}{r_p^2} ++ +\dfrac{I_s}{r_s^2} +} } -Equation \Eq{eq:tau_ns_final} is the torque required to maintain the no-slip -constraint -\Eq{eq:ratio_speed_constraint}, -or equivalently the condition -\( -v_{\Delta}=0. -\) - -% -------------------------------------------------------------- -\subsection{Section Summary} +Substituting \Eq{eq:no_slip_vb_dot} back into \Eq{eq:tau_p_ns} and +\Eq{eq:tau_s_ns} determines the corresponding no-slip torque demands at the +primary and secondary contacts. -This section derived the no-slip coupling torque $\tau_{\mathrm{ns}}$ by -specializing the rotational dynamics to the sticking branch. +\Eq{eq:no_slip_vb_dot}, \Eq{eq:tau_p_ns}, and +\Eq{eq:tau_s_ns} therefore define the no-slip candidate torque transfer. These +are the torques required to maintain adhered contact at both pulleys. They are +not yet guaranteed to be physically admissible; the next section compares these +demands against the available belt--pulley traction limits. -The derivation used three ingredients: +\subsection{Transition to Traction Limits} +\label{sec:no-slip-transition} -\begin{itemize} - \item the primary and secondary rotational equations of motion written in - terms of the internal coupling torque $\tau_c$, - \item the no-slip kinematic constraint - $\omega_p=R\,\omega_s$ and its time derivative - $\dot{\omega}_p=R\dot{\omega}_s+\omega_s\dot R$, - \item the no-slip torque mapping - $\tau_{s,\mathrm{ns}}=R\,\tau_c$. -\end{itemize} +At this point, the no-slip candidate is fully determined. For any current state, +\Eq{eq:no_slip_vb_dot} gives the belt acceleration required by adhered motion, +and \Eq{eq:tau_p_ns}--\Eq{eq:tau_s_ns} give the corresponding primary and +secondary torque demands. -Together, these relations close the sticking branch and yield the explicit -result \Eq{eq:tau_ns_final}. This quantity is the ideal adhered-branch -coupling torque: the value required to maintain sticking kinematics if -adherence can be sustained. The next sections determine whether this required -torque is admissible under the traction limits and, when it is not, how the -simulation transitions to and evolves on the slip branch. +These equations answer what torque transfer would be required to keep the belt +stuck to both pulleys. They do not yet answer whether the contacts can actually +support those torques. That question depends on the available frictional traction +generated by the axial clamping forces derived in \Sec{sec:axial}. +The next section derives the traction bounds for each pulley contact. The +no-slip demands \(\tau_{p,\mathrm{ns}}\) and \(\tau_{s,\mathrm{ns}}\) can then +be compared against those bounds to decide whether the no-slip candidate is +admissible or whether the transmission must enter a slip branch. \section{Traction Limits and Slip Conditions} -\label{sec:traction-slip} - -In \Sec{sec:transmission_regimes}, the sticking branch was defined by the -requirement that the no-slip coupling torque remain within an admissible -traction interval. In \Sec{sec:tau_ns}, the corresponding no-slip torque -$\tau_{\mathrm{ns}}$ was derived explicitly. - -The remaining task is therefore to determine the traction bounds themselves. +\label{sec:traction-limits} -Because belt--pulley friction is finite, the contact cannot sustain arbitrary -tangential traction. -For any instantaneous clamping state, only a bounded range of transmitted -coupling torque can be supported before adherence is lost. -Once the demanded torque leaves that admissible range, the no-slip branch is no -longer physically realizable and the transmission must enter slip. +The previous section derived the no-slip torque demands +\(\tau_{p,\mathrm{ns}}\) and \(\tau_{s,\mathrm{ns}}\). These are the torques +that would be required at the primary and secondary belt contacts if the belt +remained adhered to both pulleys. -Accordingly, the objective of this section is to derive bounds of the form +Those demands are not automatically attainable. Torque is transmitted through +frictional traction between the belt and the sheave faces, and this traction is +limited by the available normal force generated by axial clamping. Therefore, +each pulley contact can support only a bounded range of torque before slip must +occur. -\begin{equation} -\tau_- \le \tau \le \tau_+, -\label{eq:tau_traction_goal} -\end{equation} +The purpose of this section is to derive those pulley-wise traction bounds. For +each pulley \(j \in \{p,s\}\), we first determine the maximum admissible +tight--slack tension difference across the wrap, then convert that tension +difference into a torque bound using the effective torque radius. These bounds +will later be compared against \(\tau_{p,\mathrm{ns}}\) and +\(\tau_{s,\mathrm{ns}}\) to decide whether the no-slip candidate is admissible. -where $\tau_-$ and $\tau_+$ are the negative- and positive-direction traction -limits, respectively. -These bounds will later be compared against the no-slip torque -$\tau_{\mathrm{ns}}$ to determine whether the transmission remains in stick or -transitions to slip. % -------------------------------------------------------------- \subsection{Tight--Slack Tension Difference and Torque Capacity} \label{sec:traction_overview} @@ -5866,9 +5385,9 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} \qquad \dot r_{j,\mathrm{cm}}(t), \qquad -\hat v_b(t), +v_b(t), \qquad -\dot{\hat v}_b(t). +\dot{v}_b(t). \] \paragraph{Coordinate system and unit vectors} @@ -5886,13 +5405,6 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} The tangential direction of belt motion is therefore exactly \(\mathbf e_{\theta,j}\). -\begin{figure}[H] - \centering - \includegraphics[width=0.7\textwidth]{./illustrations/slip/polar.png} - \caption{Definition of the radial and tangential unit vectors.} - \label{fig:polar_basis_placeholder} -\end{figure} - \paragraph{Tangential acceleration as a component of \(\mathbf a\)} Tangential acceleration is the component of the acceleration vector along @@ -5914,7 +5426,7 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} \end{equation} The belt transport speed is the tangential speed of the belt element along the -wrap. Since the modeled belt transport speed is \(\hat v_b\), the velocity of +wrap. Since the modeled belt transport speed is \(v_b\), the velocity of the belt element centroid is \begin{equation} @@ -5922,11 +5434,11 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} = \dot r_{j,\mathrm{cm}}\,\mathbf e_{r,j} + -\hat v_b\,\mathbf e_{\theta,j}. +v_b\,\mathbf e_{\theta,j}. \label{eq:velocity_polar_vb} \end{equation} -Thus, \(\dot r_{j,\mathrm{cm}}\) is the radial speed and \(\hat v_b\) is the +Thus, \(\dot r_{j,\mathrm{cm}}\) is the radial speed and \(v_b\) is the tangential speed. \paragraph{Step 2: time derivatives of the unit vectors} @@ -5947,7 +5459,7 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} = \omega_{b,j} = -\frac{\hat v_b}{r_{j,\mathrm{cm}}}. +\frac{v_b}{r_{j,\mathrm{cm}}}. \label{eq:omega_bj_repeat} \end{equation} @@ -5955,12 +5467,12 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} \begin{equation} \dot{\mathbf e}_{r,j} = -\frac{\hat v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{\theta,j}, +\frac{v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{\theta,j}, \qquad \dot{\mathbf e}_{\theta,j} = - -\frac{\hat v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{r,j}. +\frac{v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{r,j}. \label{eq:unit_vectors_time_derivatives_vb} \end{equation} @@ -5972,16 +5484,16 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} &= \frac{\dd}{\dd t}\big(\dot r_{j,\mathrm{cm}}\,\mathbf e_{r,j}\big) + -\frac{\dd}{\dd t}\big(\hat v_b\,\mathbf e_{\theta,j}\big) +\frac{\dd}{\dd t}\big(v_b\,\mathbf e_{\theta,j}\big) \nonumber\\[6pt] &= \ddot r_{j,\mathrm{cm}}\,\mathbf e_{r,j} + \dot r_{j,\mathrm{cm}}\,\dot{\mathbf e}_{r,j} + -\dot{\hat v}_b\,\mathbf e_{\theta,j} +\dot{v}_b\,\mathbf e_{\theta,j} + -\hat v_b\,\dot{\mathbf e}_{\theta,j}. +v_b\,\dot{\mathbf e}_{\theta,j}. \label{eq:accel_expand_vb} \end{align} @@ -5993,27 +5505,27 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} + \dot r_{j,\mathrm{cm}} \left( -\frac{\hat v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{\theta,j} +\frac{v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{\theta,j} \right) + -\dot{\hat v}_b\,\mathbf e_{\theta,j} +\dot{v}_b\,\mathbf e_{\theta,j} - -\hat v_b +v_b \left( -\frac{\hat v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{r,j} +\frac{v_b}{r_{j,\mathrm{cm}}}\,\mathbf e_{r,j} \right) \nonumber\\[6pt] &= \left( \ddot r_{j,\mathrm{cm}} - -\frac{\hat v_b^2}{r_{j,\mathrm{cm}}} +\frac{v_b^2}{r_{j,\mathrm{cm}}} \right)\mathbf e_{r,j} + \left( -\dot{\hat v}_b +\dot{v}_b + -\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}\hat v_b +\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}v_b \right)\mathbf e_{\theta,j}. \label{eq:accel_polar_vb} \end{align} @@ -6023,15 +5535,15 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} \begin{equation} a_{t,j} = -\dot{\hat v}_b +\dot{v}_b + -\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}\hat v_b. +\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}v_b. \label{eq:at_final} \end{equation} \paragraph{Interpretation} -The first term, \(\dot{\hat v}_b\), is the direct time rate of change of the +The first term, \(\dot{v}_b\), is the direct time rate of change of the belt transport speed. The second term appears because the belt element is not only moving along the wrap, but also moving to a different wrap radius at the same time. As the element shifts @@ -6041,7 +5553,7 @@ \subsection{Tangential Acceleration of a Belt Element on a Circular Wrap} The radial component \( -\ddot r_{j,\mathrm{cm}} - \hat v_b^2/r_{j,\mathrm{cm}} +\ddot r_{j,\mathrm{cm}} - v_b^2/r_{j,\mathrm{cm}} \) contains the centripetal term, but it does not enter the tangential force balance used in \Sec{sec:traction_element}. @@ -6054,54 +5566,15 @@ \subsection{Normal Force from Axial Clamping} \[ N_{\phi,j} \equiv \int_{-\phi_j/2}^{\phi_j/2} \dd N_j. \] -We now express \(N_{\phi,j}\) in terms of the axial clamping force -generated by that pulley. +We now express \(N_{\phi,j}\) in terms of the axial clamping force generated by that pulley. % -------------------------------------------------------------- \paragraph{Axial clamp produces radial normal load} -The belt is pressed between two opposing conical sheaves. -On pulley \(j\), an axial clamping force \(F_{\mathrm{ax},j}\) pushes the -sheaves together. -Because the sheave faces are inclined at cone half-angle \(\beta\), -this axial force generates a radial normal load on the belt. -Here, \(F_{\mathrm{ax},j}\) is clamping force only; belt inertia -(centrifugal effects) is already accounted for by the acceleration term in -\Eq{eq:deltaT_final}. - -From the conical geometry relation established in -\Eq{eq:cone_geometry_diff}, - -\[ -\frac{\dd r_j}{\dd s} = \frac{1}{2\tan\beta}. -\] - -To relate the axial and radial forces consistently, we use virtual work. -The incremental work done by the axial clamp must equal the incremental -work associated with radial compression of the belt: - -\[ -F_{\mathrm{ax},j}\,\dd s -= -F_{\mathrm{rad},j}\,\dd r_j. -\] - -Substituting -\( -\dd r_j = \left(\dd r_j/\dd s\right)\dd s -\) -gives - -\begin{equation} -F_{\mathrm{rad},j} -= -\frac{F_{\mathrm{ax},j}}{\dd r_j/\dd s} -= -2F_{\mathrm{ax},j}\tan\beta. -\label{eq:Frad_from_Fax} -\end{equation} - -Thus, the axial clamp force on pulley \(j\) produces a total radial normal load +The belt is pressed between two opposing conical sheaves; the two inclined faces +apply a net normal force on the belt equal to the inward radial load generated by +the axial clamp. The same geometry was considered before, and we can rearrange +\Eq{eq:radial_to_axial_conversion} to get \begin{equation} F_{\mathrm{rad},j} = 2F_{\mathrm{ax},j}\tan\beta. @@ -6156,9 +5629,9 @@ \subsection{Torque Capacity} \qquad a_{t,j} = -\dot{\hat v}_b +\dot{v}_b + -\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}\hat v_b, +\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}v_b, \] we obtain \begin{equation} @@ -6168,9 +5641,9 @@ \subsection{Torque Capacity} - \rho_b A_b \phi_j r_{j,\mathrm{cm}} \left( -\dot{\hat v}_b +\dot{v}_b + -\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}\hat v_b +\frac{\dot r_{j,\mathrm{cm}}}{r_{j,\mathrm{cm}}}v_b \right). \label{eq:deltaT_max_explicit} \end{equation} @@ -6187,9 +5660,9 @@ \subsection{Torque Capacity} - \rho_b A_b \phi_j \left( -r_{j,\mathrm{cm}}\dot{\hat v}_b +r_{j,\mathrm{cm}}\dot{v}_b + -\dot r_{j,\mathrm{cm}}\,\hat v_b +\dot r_{j,\mathrm{cm}}\,v_b \right) \right]. } @@ -6211,1108 +5684,1283 @@ \subsection{Torque Capacity} in \Eq{eq:tau_capacity_final}. \end{remarkbox} -\subsection{Primary Pulley Traction Bounds} +\subsection{Primary Pulley No-Slip Admissibility} \label{sec:primary_tau_limits} -The generic capacity law \Eq{eq:tau_capacity_final} must now be specialized to -the primary pulley. -Because the inertial correction contains \(\dot{\hat v}_b\), and the belt-speed -derivative is defined differently in stick and slip, the primary traction bounds -must be derived separately for the two transmission regimes. - -In the sticking regime, the belt speed is imposed by compatibility. -Accordingly, we use the primary-side sticking derivative from -\Eq{eq:vb_dot_stick_primary}, together with the primary axial-force model -\Eq{eq:primary_axial}. -This causes the inertial term to depend on the primary angular acceleration, -and therefore on the coupling torque itself. -The resulting sticking bound is thus implicit in \(\tau_c\), but it can still -be solved in closed form. - -In the slipping regime, the belt-speed derivative is instead given directly by -the regularized evolution law \Eq{eq:vb_relaxation}. -In that case, the primary traction bound no longer depends on \(\tau_c\) -through the inertial term, so the upper and lower limits follow directly. +The generic capacity law \Eq{eq:tau_capacity_final} can now be specialized to +the primary pulley. At this stage, the goal is not to determine the slipping +motion. The goal is only to test whether the no-slip demand derived in +\Sec{sec:no-slip-torque-demand} can be supported by the available primary +traction. -% -------------------------------------------------------------- -\paragraph{Stick regime} +For compactness in this subsection, write +\[ +r_p = r_{p,\mathrm{eff}}(s). +\] +On the no-slip candidate branch, the belt acceleration is +\(\dot v_{b,\mathrm{ns}}\), as given by \Eq{eq:no_slip_vb_dot}. Therefore, +the primary traction capacity is evaluated using this candidate acceleration. -On the primary pulley, the transmitted torque at the belt contact is the -coupling torque itself. -Applying \Eq{eq:tau_capacity_final} with \(j=p\) gives +Applying \Eq{eq:tau_capacity_final} with \(j=p\), and using the static friction +coefficient \(\mu_s\), gives -\begin{equation} -|\tau_c| -\le -r_{p,\mathrm{eff}} +\bigeq{eq:tau_p_max_stick_general}{Primary No-Slip Torque Capacity}{ +\tau_{p,\max}^{\mathrm{stick}} += +r_p \left[ -2\mu F_{\mathrm{ax},p}\tan\beta +2\mu_s F_{\mathrm{ax},p}(s,\omega_p)\tan\beta - \rho_b A_b \phi_p \left( -r_{p,\mathrm{cm}}\dot{\hat v}_b +r_{p,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -\dot r_{p,\mathrm{cm}}\,\hat v_b +\dot r_{p,\mathrm{cm}}\,v_b \right) -\right]. -\label{eq:tau_p_capacity_stick_start} -\end{equation} +\right] +} -In the sticking regime, +The primary axial force is known explicitly from \Eq{eq:primary_axial}: \[ -\hat v_b = r_{p,\mathrm{cm}}\omega_p, -\qquad -\dot{\hat v}_b +F_{\mathrm{ax},p}(s,\omega_p) = -\dot r_{p,\mathrm{cm}}\,\omega_p -+ -r_{p,\mathrm{cm}}\,\dot\omega_p, +m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} +- +k_p\bigl(x_{p,0}+s\bigr). \] -from \Eq{eq:vb_stick_exact} and \Eq{eq:vb_dot_stick_primary}. -Substituting these into \Eq{eq:tau_p_capacity_stick_start} gives -\begin{equation} -|\tau_c| -\le -r_{p,\mathrm{eff}} -\left[ -2\mu F_{\mathrm{ax},p}\tan\beta +Substituting this force model into \Eq{eq:tau_p_max_stick_general} gives the +fully explicit primary no-slip traction capacity: + +\widebigeq{eq:tau_p_max_stick}{Primary No-Slip Torque Capacity (Expanded)}{ +\tau_{p,\max}^{\mathrm{stick}} += +r_p +\Biggl[ +2\mu_s\tan\beta +\left( +m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\dfrac{\dd r_f}{\dd s} +- +k_p\bigl(x_{p,0}+s\bigr) +\right) - \rho_b A_b \phi_p \left( -r_{p,\mathrm{cm}}^2\dot\omega_p +r_{p,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p +\dot r_{p,\mathrm{cm}}\,v_b \right) -\right]. -\label{eq:tau_p_capacity_stick_mid} -\end{equation} +\Biggr] +} + +This quantity is the maximum primary contact torque magnitude that can be +supported while maintaining the no-slip candidate motion. The primary no-slip +torque demand from \Eq{eq:tau_p_ns} is therefore admissible only if -Now substitute the primary axial-force model from \Eq{eq:primary_axial}, -\begin{equation} -F_{\mathrm{ax},p}(s,\omega_p) -= -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr), -\label{eq:Fax_p_sub_stick} -\end{equation} -together with the primary rotational equation of motion from \Eq{eq:primary_rot_dyn} \begin{equation} -\dot\omega_p -= -\frac{\tau_{\mathrm{eng}}-\tau_c}{I_p}. -\label{eq:omega_p_dot_sub_stick} +\boxed{ +\left|\tau_{p,\mathrm{ns}}\right| +\le +\tau_{p,\max}^{\mathrm{stick}}. +} +\label{eq:primary_no_slip_admissibility} \end{equation} -This yields +Equivalently, +\[ +-\tau_{p,\max}^{\mathrm{stick}} +\le +\tau_{p,\mathrm{ns}} +\le +\tau_{p,\max}^{\mathrm{stick}}. +\] + +If \Eq{eq:primary_no_slip_admissibility} is satisfied, the primary contact can +support the torque required by the no-slip candidate. If it is not satisfied, +the primary contact cannot remain adhered under the demanded torque, and the +final torque transfer must instead be determined by a slipping contact law. + + +\subsection{Secondary Pulley No-Slip Admissibility} +\label{sec:secondary_tau_limits} + +The secondary pulley follows the same overall procedure as the primary, but +with one important difference: the secondary axial force is torque-reactive. +Unlike the primary axial force, which depends only on shift position and primary +speed, the secondary axial force depends on the same transmitted torque being +checked for admissibility. + +For compactness in this subsection, write +\[ +r_s = r_{s,\mathrm{eff}}(s). +\] +On the no-slip candidate branch, the belt acceleration is +\(\dot v_{b,\mathrm{ns}}\), as given by \Eq{eq:no_slip_vb_dot}. Therefore, +the secondary traction capacity is evaluated using this candidate acceleration. + +Applying \Eq{eq:tau_capacity_final} with \(j=s\), and using the static friction +coefficient \(\mu_s\), gives \begin{equation} -\begin{aligned} -|\tau_c| -\le\;& -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta +\tau_{s,\max}^{\mathrm{stick}} += +r_s \left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] -\\[4pt] -&\qquad +2\mu_s F_{\mathrm{ax},s}(s,\tau_s)\tan\beta - -\rho_b A_b \phi_p -\left[ -\frac{r_{p,\mathrm{cm}}^2}{I_p}\bigl(\tau_{\mathrm{eng}}-\tau_c\bigr) +\rho_b A_b \phi_s +\left( +r_{s,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\}. -\end{aligned} -\label{eq:tau_p_capacity_stick_explicit} +\dot r_{s,\mathrm{cm}}\,v_b +\right) +\right]. +\label{eq:tau_s_max_stick_general} \end{equation} -The coupling torque now appears on both sides of the inequality, so the -positive and negative directions must again be treated separately. - -\paragraph{Positive-direction sticking bound \(\tau_c \ge 0\)} - -For \(\tau_c \ge 0\), \(|\tau_c|=\tau_c\), so \Eq{eq:tau_p_capacity_stick_explicit} -becomes +For the no-slip admissibility check, the torque appearing in the secondary +axial force model is the no-slip demand \(\tau_{s,\mathrm{ns}}\) from +\Eq{eq:tau_s_ns}. Using the secondary axial force model from +\Eq{eq:secondary_axial}, +\[ +F_{\mathrm{ax},s}(s,\tau_{s,\mathrm{ns}}) += +\frac{ +\tau_{s,\mathrm{ns}} ++ +k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +}{2} +\frac{\dd\theta_s}{\dd s} ++ +k_{s,x}(x_{s,0}+s). +\] +Substituting this into \Eq{eq:tau_s_max_stick_general} gives \begin{equation} \begin{aligned} -\tau_c -\le\;& -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] -\\[4pt] -&\qquad -- -\rho_b A_b \phi_p -\left[ -\frac{r_{p,\mathrm{cm}}^2}{I_p}\tau_{\mathrm{eng}} +\tau_{s,\max}^{\mathrm{stick}} += +r_s +\Biggl[ +&\mu_s\tan\beta +\left( +\tau_{s,\mathrm{ns}} ++ +k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\right) +\frac{\dd\theta_s}{\dd s} +\\ +&+ +2\mu_s\tan\beta\,k_{s,x}(x_{s,0}+s) - -\frac{r_{p,\mathrm{cm}}^2}{I_p}\tau_c +\rho_b A_b \phi_s +\left( +r_{s,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\}. +\dot r_{s,\mathrm{cm}}\,v_b +\right) +\Biggr]. \end{aligned} -\label{eq:tau_p_plus_stick_expand} +\label{eq:tau_s_max_stick_expanded} \end{equation} -Collecting the \(\tau_c\) terms on the left gives +This expression shows why the secondary admissibility check is slightly +different from the primary. The capacity depends linearly on the no-slip torque +demand itself, because a larger transmitted secondary torque also increases the +torque-reactive helix clamping force. + +The secondary no-slip demand is admissible if +\[ +|\tau_{s,\mathrm{ns}}| +\le +\tau_{s,\max}^{\mathrm{stick}}. +\] +For a positive no-slip secondary torque demand, +\(\tau_{s,\mathrm{ns}}\ge 0\), substituting +\Eq{eq:tau_s_max_stick_expanded} gives \begin{equation} \begin{aligned} -\Biggl( -1 -- -r_{p,\mathrm{eff}}\rho_b A_b \phi_p -\frac{r_{p,\mathrm{cm}}^2}{I_p} -\Biggr)\tau_c -\le\;& -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] -\\[4pt] -&\qquad +\tau_{s,\mathrm{ns}} +\le +r_s +\Biggl[ +&\mu_s\tan\beta +\left( +\tau_{s,\mathrm{ns}} ++ +k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\right) +\frac{\dd\theta_s}{\dd s} +\\ +&+ +2\mu_s\tan\beta\,k_{s,x}(x_{s,0}+s) - -\rho_b A_b \phi_p -\left[ -\frac{r_{p,\mathrm{cm}}^2}{I_p}\tau_{\mathrm{eng}} +\rho_b A_b \phi_s +\left( +r_{s,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\}. +\dot r_{s,\mathrm{cm}}\,v_b +\right) +\Biggr]. \end{aligned} -\label{eq:tau_p_plus_stick_rearrange} +\label{eq:tau_s_positive_admissibility_expand} \end{equation} -Hence, -\begin{equation} -\tau_c \le \tau_{c,p,+}^{\mathrm{stick}}, -\label{eq:tau_p_plus_stick_ineq} -\end{equation} -with +Collecting the \(\tau_{s,\mathrm{ns}}\) terms gives the positive-direction +upper bound -\bigeq{eq:tau_p_plus_stick}{Primary Stick-Branch Upper Bound}{ -\tau_{c,p,+}^{\mathrm{stick}} +\widebigeq{eq:tau_s_plus_stick}{Secondary No-Slip Upper Bound}{ +\tau_{s,+}^{\mathrm{stick}} = \frac{ -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\dfrac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] +r_s +\Biggl[ +\mu_s\tan\beta\, +k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\dfrac{\dd\theta_s}{\dd s} ++ +2\mu_s\tan\beta\,k_{s,x}(x_{s,0}+s) - -\rho_b A_b \phi_p -\left[ -\dfrac{r_{p,\mathrm{cm}}^2}{I_p}\tau_{\mathrm{eng}} +\rho_b A_b \phi_s +\left( +r_{s,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\} +\dot r_{s,\mathrm{cm}}\,v_b +\right) +\Biggr] }{ 1 - -r_{p,\mathrm{eff}}\rho_b A_b \phi_p -\dfrac{r_{p,\mathrm{cm}}^2}{I_p} -}. +r_s\mu_s\tan\beta +\dfrac{\dd\theta_s}{\dd s} +} } -\paragraph{Negative-direction sticking bound \(\tau_c \le 0\)} - -For \(\tau_c \le 0\), \(|\tau_c|=-\tau_c\), so \Eq{eq:tau_p_capacity_stick_explicit} -becomes +For a negative no-slip secondary torque demand, +\(\tau_{s,\mathrm{ns}}\le 0\), the admissibility condition becomes +\[ +-\tau_{s,\mathrm{ns}} +\le +\tau_{s,\max}^{\mathrm{stick}}. +\] +Substituting \Eq{eq:tau_s_max_stick_expanded} gives \begin{equation} \begin{aligned} -\Biggl( -1 -+ -r_{p,\mathrm{eff}}\rho_b A_b \phi_p -\frac{r_{p,\mathrm{cm}}^2}{I_p} -\Biggr)\tau_c -\ge\;& -- -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] -\\[4pt] -&\qquad -- -\rho_b A_b \phi_p -\left[ -\frac{r_{p,\mathrm{cm}}^2}{I_p}\tau_{\mathrm{eng}} -+ -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\}. -\end{aligned} -\label{eq:tau_p_minus_stick_rearrange} -\end{equation} - -Collecting the \(\tau_c\) terms gives - -\begin{equation} +-\tau_{s,\mathrm{ns}} +\le +r_s +\Biggl[ +&\mu_s\tan\beta \left( -1 +\tau_{s,\mathrm{ns}} + -r_{p,\mathrm{eff}}\rho_b A_b \phi_p -\frac{r_{p,\mathrm{cm}}^2}{I_p} -\right)\tau_c -\ge -- -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] +k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\right) +\frac{\dd\theta_s}{\dd s} +\\ +&+ +2\mu_s\tan\beta\,k_{s,x}(x_{s,0}+s) - -\rho_b A_b \phi_p -\left[ -\frac{r_{p,\mathrm{cm}}^2}{I_p}\tau_{\mathrm{eng}} +\rho_b A_b \phi_s +\left( +r_{s,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\}. -\label{eq:tau_p_minus_stick_rearrange_2} +\dot r_{s,\mathrm{cm}}\,v_b +\right) +\Biggr]. +\end{aligned} +\label{eq:tau_s_negative_admissibility_expand} \end{equation} -Hence, -\begin{equation} -\tau_c \ge \tau_{c,p,-}^{\mathrm{stick}}, -\label{eq:tau_p_minus_stick_ineq} -\end{equation} -with +Collecting the \(\tau_{s,\mathrm{ns}}\) terms gives the negative-direction +lower bound -\bigeq{eq:tau_p_minus_stick}{Primary Stick-Branch Lower Bound}{ -\tau_{c,p,-}^{\mathrm{stick}} +\widebigeq{eq:tau_s_minus_stick}{Secondary No-Slip Lower Bound}{ +\tau_{s,-}^{\mathrm{stick}} = -\frac{ -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\dfrac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] +r_s +\Biggl[ +\mu_s\tan\beta\, +k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\dfrac{\dd\theta_s}{\dd s} ++ +2\mu_s\tan\beta\,k_{s,x}(x_{s,0}+s) - -\rho_b A_b \phi_p -\left[ -\dfrac{r_{p,\mathrm{cm}}^2}{I_p}\tau_{\mathrm{eng}} +\rho_b A_b \phi_s +\left( +r_{s,\mathrm{cm}}\dot v_{b,\mathrm{ns}} + -2r_{p,\mathrm{cm}}\dot r_{p,\mathrm{cm}}\,\omega_p -\right] -\Biggr\} +\dot r_{s,\mathrm{cm}}\,v_b +\right) +\Biggr] }{ -\displaystyle 1 + -r_{p,\mathrm{eff}}\rho_b A_b \phi_p -\dfrac{r_{p,\mathrm{cm}}^2}{I_p} -}. +r_s\mu_s\tan\beta +\dfrac{\dd\theta_s}{\dd s} +} } -Therefore, in the sticking regime the primary pulley contributes the interval +The secondary no-slip torque demand is therefore admissible only if + \begin{equation} \boxed{ -\tau_{c,p,-}^{\mathrm{stick}} +\tau_{s,-}^{\mathrm{stick}} \le -\tau_c +\tau_{s,\mathrm{ns}} \le -\tau_{c,p,+}^{\mathrm{stick}}. +\tau_{s,+}^{\mathrm{stick}}. } -\label{eq:primary_tau_interval_stick} +\label{eq:secondary_no_slip_admissibility} \end{equation} -% -------------------------------------------------------------- -\paragraph{Slip regime} +If \Eq{eq:secondary_no_slip_admissibility} is satisfied, the secondary contact +can support the torque required by the no-slip candidate. If it is not +satisfied, the secondary contact cannot remain adhered under the demanded +torque, and the final torque transfer must instead be determined by a slipping +contact law. -In the slipping regime, the belt-speed derivative is given directly by the -regularized closure \Eq{eq:vb_relaxation}: -\[ -\dot{\hat v}_b +\subsection{Section Conclusion and Transition} +\label{sec:traction_conclusion} + +This section derived the traction limits needed to test the no-slip torque +candidate from \Sec{sec:no-slip-belt-dynamics}. Starting from a local +belt--pulley force balance, the available tight--slack tension difference was +converted into a pulley torque capacity. This produced admissibility conditions +for the primary and secondary no-slip torque demands, +\(\tau_{p,\mathrm{ns}}\) and \(\tau_{s,\mathrm{ns}}\). + +These results determine whether the no-slip candidate can be physically +maintained. If both pulley contacts satisfy their admissibility conditions, the +belt can remain adhered to both pulleys and the no-slip torque transfer is +accepted. + +What remains is the complementary case: what happens when one or both contacts +cannot remain adhered, or when relative belt--pulley motion is already present. +In that situation, the torque transfer is no longer determined by the no-slip +demand. Instead, the slipping contact must be governed by kinetic traction and +the belt equation of motion. The next section develops the contact-state +selection rules and the corresponding slip-branch dynamics. + + + +\section{Slip Branch Dynamics and Contact-State Selection} +\label{sec:slip-branch-dynamics} + +The previous section determined when the no-slip torque demands can be +supported by the available belt--pulley traction. If the primary and secondary +contacts both satisfy their admissibility conditions, the no-slip candidate is +physically consistent and the belt remains adhered to both pulleys. + +When one of these admissibility conditions fails, the no-slip candidate can no +longer be used as the final torque-transfer law. The contact that exceeds its +available traction cannot supply the demanded torque while remaining stuck, so +relative motion begins at that belt--pulley interface. Once this occurs, the +contact torque is no longer determined by the no-slip demand. Instead, it is set +by the kinetic traction available at the slipping contact. + +This section develops the rules used to select the active contact state and the +corresponding equations of motion. The same three bodies introduced in +\Sec{sec:belt-torque-path} are retained: the primary pulley, the belt, and the +secondary pulley. What changes from one branch to another is the contact +condition imposed at each pulley. A stuck contact enforces belt-speed +compatibility, while a slipping contact applies a saturated kinetic traction +torque in the direction opposing relative motion. + +The goal is therefore to complete the torque-transfer closure. We first define +the relative belt--pulley speeds used to identify sliding, then state the rules +for entering, maintaining, and exiting slip. The remaining subsections derive the +branch dynamics for primary slip, secondary slip, and simultaneous slip at both +contacts. + +\subsection{Relative Contact Speeds and Slip-State Logic} +\label{sec:relative-contact-speeds} + +To determine whether a belt--pulley contact is sticking or slipping, the model +compares the belt transport speed to the local surface speed imposed by each +pulley. + +For the primary pulley, define the relative contact speed as +\begin{equation} +v_{\mathrm{rel},p} = -\frac{v_b^\ast-\hat v_b}{T_b}. -\] +r_p\omega_p - v_b, +\label{eq:v_rel_primary} +\end{equation} +where \(r_p = r_{p,\mathrm{eff}}(s)\). Thus, \(v_{\mathrm{rel},p}>0\) means +the primary pulley surface is moving faster than the belt at the contact radius +and tends to drive the belt forward. + +For the secondary pulley, define +\begin{equation} +v_{\mathrm{rel},s} += +v_b - r_s\omega_s, +\label{eq:v_rel_secondary} +\end{equation} +where \(r_s = r_{s,\mathrm{eff}}(s)\). Thus, \(v_{\mathrm{rel},s}>0\) means +the belt is moving faster than the secondary pulley surface and tends to drive +the secondary forward. -Applying \Eq{eq:tau_capacity_final} with \(j=p\), and again substituting the -primary axial-force model \Eq{eq:primary_axial}, gives +These definitions are chosen so that positive relative speed corresponds to the +usual forward torque-transfer direction at each contact: primary-to-belt on the +primary side, and belt-to-secondary on the secondary side. +A contact can remain stuck only if its relative contact speed is zero, or within +a small numerical tolerance: \begin{equation} -|\tau_c| +|v_{\mathrm{rel},j}| \le \varepsilon_v, +\qquad j\in\{p,s\}. +\label{eq:stick_velocity_tolerance} +\end{equation} + +If both contacts satisfy \Eq{eq:stick_velocity_tolerance}, the model first +tests the no-slip candidate derived in \Sec{sec:no-slip-belt-dynamics}. This +candidate is accepted only if both pulley contacts also satisfy the traction +admissibility conditions derived in \Sec{sec:traction-limits}: +\begin{equation} +|\tau_{p,\mathrm{ns}}| \le -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\frac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] -- -\rho_b A_b \phi_p -\left[ -r_{p,\mathrm{cm}}\frac{v_b^\ast-\hat v_b}{T_b} -+ -\dot r_{p,\mathrm{cm}}\,\hat v_b -\right] -\Biggr\}. -\label{eq:tau_p_capacity_slip_explicit} +\tau_{p,\max}^{\mathrm{stick}}, +\label{eq:primary_stick_acceptance_repeat} +\end{equation} +and +\begin{equation} +\tau_{s,-}^{\mathrm{stick}} +\le +\tau_{s,\mathrm{ns}} +\le +\tau_{s,+}^{\mathrm{stick}}. +\label{eq:secondary_stick_acceptance_repeat} \end{equation} -In contrast to the sticking case, the right-hand side no longer depends on -\(\tau_c\). -The positive and negative limits therefore follow directly. +If these conditions hold, the no-slip candidate becomes the active +torque-transfer branch: +\[ +\tau_p = \tau_{p,\mathrm{ns}}, +\qquad +\tau_s = \tau_{s,\mathrm{ns}}, +\qquad +\dot v_b = \dot v_{b,\mathrm{ns}}. +\] -\paragraph{Positive-direction slipping bound \(\tau_c \ge 0\)} +If either contact fails its admissibility check, that contact cannot support the +required no-slip torque and must enter slip. The direction of slip is determined +by the direction in which the demanded torque exceeds the admissible interval. +For the primary pulley, +\[ +\tau_{p,\mathrm{ns}} > \tau_{p,\max}^{\mathrm{stick}} +\quad\Rightarrow\quad +\text{positive primary slip}, +\] +while +\[ +\tau_{p,\mathrm{ns}} < -\tau_{p,\max}^{\mathrm{stick}} +\quad\Rightarrow\quad +\text{negative primary slip}. +\] -For \(\tau_c \ge 0\), \(|\tau_c|=\tau_c\), so +For the secondary pulley, +\[ +\tau_{s,\mathrm{ns}} > \tau_{s,+}^{\mathrm{stick}} +\quad\Rightarrow\quad +\text{positive secondary slip}, +\] +while +\[ +\tau_{s,\mathrm{ns}} < \tau_{s,-}^{\mathrm{stick}} +\quad\Rightarrow\quad +\text{negative secondary slip}. +\] + +Once a contact is already sliding, its state is determined primarily by its +relative contact speed. If +\[ +|v_{\mathrm{rel},j}| > \varepsilon_v, +\] +then contact \(j\) is treated as slipping, and its torque is set by kinetic +traction rather than by the no-slip demand. The slip direction is recorded by \begin{equation} -\tau_c \le \tau_{c,p,+}^{\mathrm{slip}}, -\label{eq:tau_p_plus_slip_ineq} +\sigma_j = \operatorname{sgn}(v_{\mathrm{rel},j}), +\qquad j\in\{p,s\}. +\label{eq:slip_direction_sigma} \end{equation} -with -\bigeq{eq:tau_p_plus_slip}{Primary Slip-Branch Upper Bound}{ -\tau_{c,p,+}^{\mathrm{slip}} +Slip persists while the relative contact speed remains nonzero. During this +time, the slipping contact applies a saturated kinetic traction torque in the +direction opposing relative motion. The no-slip candidate is not reaccepted +until the relative speed returns to the sticking tolerance and the static +admissibility conditions can again be satisfied. + +This gives three possible slipping branches: +\begin{itemize} + \item primary slip, where the primary contact slides and the secondary + contact remains stuck; + \item secondary slip, where the secondary contact slides and the primary + contact remains stuck; + \item two-contact slip, where both pulley contacts slide simultaneously. +\end{itemize} + +The following subsections derive the equations of motion for each branch. + +\begin{remarkbox}[Remark (Power Flow During Slip)] +In the no-slip branch, belt-speed compatibility constrains the primary pulley, +belt, and secondary pulley to move together kinematically. Once slip occurs, +that compatibility is lost at the slipping contact. The contact torque is then +limited by kinetic traction, while the pulley and belt speeds evolve according +to their own equations of motion. + +As a result, the instantaneous mechanical power at the primary contact, +\(\tau_p\omega_p\), does not need to match the instantaneous mechanical power at +the secondary contact, \(\tau_s\omega_s\). The difference is associated with +changes in stored kinetic energy of the rotating components and belt, together +with slip dissipation. Thus, an apparent short-term excess of output-side power +does not imply energy creation; it reflects energy being drawn from inertia +while the slipping contact breaks the ideal no-slip power constraint. +\end{remarkbox} + +\subsection{Kinetic Traction Law for a Slipping Contact} +\label{sec:kinetic-traction-law} + +Once a belt--pulley contact is slipping, the torque at that contact is no +longer determined by the no-slip torque demand. Instead, the contact applies the +largest kinetic traction torque available in the direction opposing relative +motion. + +The generic torque-capacity expression from \Eq{eq:tau_capacity_final} still +applies, but the static coefficient \(\mu_s\) is replaced by the kinetic +coefficient \(\mu_k\). For a slipping contact on pulley \(j\in\{p,s\}\), define + +\begin{equation} +\tau_{j,\max}^{\mathrm{slip}}(\dot v_b) = -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta +r_j \left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\dfrac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] +2\mu_k F_{\mathrm{ax},j}\tan\beta - -\rho_b A_b \phi_p -\left[ -r_{p,\mathrm{cm}}\dfrac{v_b^\ast-\hat v_b}{T_b} +\rho_b A_b \phi_j +\left( +r_{j,\mathrm{cm}}\dot v_b + -\dot r_{p,\mathrm{cm}}\,\hat v_b -\right] -\Biggr\}. -} +\dot r_{j,\mathrm{cm}}\,v_b +\right) +\right], +\label{eq:tau_j_max_slip_general} +\end{equation} + +where \(r_j=r_{j,\mathrm{eff}}(s)\). The notation +\(\tau_{j,\max}^{\mathrm{slip}}(\dot v_b)\) emphasizes that the available +slipping torque is not generally a fixed number; it depends on the belt +acceleration through the inertial correction term. -\paragraph{Negative-direction slipping bound \(\tau_c \le 0\)} +The direction of the slipping torque is determined by the relative contact speed +defined in \Sec{sec:relative-contact-speeds}. Let -For \(\tau_c \le 0\), \(|\tau_c|=-\tau_c\), so \begin{equation} -\tau_c \ge \tau_{c,p,-}^{\mathrm{slip}}, -\label{eq:tau_p_minus_slip_ineq} +\sigma_j += +\operatorname{sgn}\!\left(v_{\mathrm{rel},j}\right), +\qquad j\in\{p,s\}. +\label{eq:sigma_j_def} \end{equation} -with -\bigeq{eq:tau_p_minus_slip}{Primary Slip-Branch Lower Bound}{ -\tau_{c,p,-}^{\mathrm{slip}} -= -- -r_{p,\mathrm{eff}} -\Biggl\{ -2\mu\tan\beta -\left[ -m_f\omega_p^2\,\big(r_{f,0}+r_f(s)\big)\,\dfrac{\dd r_f}{\dd s} -- -k_p\bigl(x_{p,0}+s\bigr) -\right] -- -\rho_b A_b \phi_p -\left[ -r_{p,\mathrm{cm}}\dfrac{v_b^\ast-\hat v_b}{T_b} -+ -\dot r_{p,\mathrm{cm}}\,\hat v_b -\right] -\Biggr\}. -} +With the relative-speed definitions adopted in \Eq{eq:v_rel_primary} and +\Eq{eq:v_rel_secondary}, \(\sigma_j>0\) corresponds to the usual forward +torque-transfer direction at contact \(j\). The slipping contact torque is then +modeled as the saturated kinetic traction torque -Therefore, in the slipping regime the primary pulley contributes the interval \begin{equation} -\boxed{ -\tau_{c,p,-}^{\mathrm{slip}} -\le -\tau_c -\le -\tau_{c,p,+}^{\mathrm{slip}}. -} -\label{eq:primary_tau_interval_slip} +\tau_j += +\sigma_j\,\tau_{j,\max}^{\mathrm{slip}}(\dot v_b). +\label{eq:tau_j_slip_saturated} \end{equation} -% -------------------------------------------------------------- -\subsection{Secondary Pulley Traction Bounds} -\label{sec:secondary_tau_limits} +Equation \Eq{eq:tau_j_slip_saturated} should not be interpreted as a completed +solution for \(\tau_j\), because the right-hand side still contains +\(\dot v_b\). In a slip branch, the belt acceleration must be solved +simultaneously with the saturated contact torque using the belt equation of +motion \Eq{eq:belt_transport_eom}. The following subsections carry out this +solve for primary slip, secondary slip, and simultaneous slip at both contacts. -The secondary pulley follows the same overall procedure as the primary, but -with one important difference. -At the secondary, the torque at the belt contact is not treated as an -independent quantity. -Instead, from the coupling relation introduced in \Eq{eq:torque_ratio}, -\begin{equation} -\tau_s = R\,\tau_c. -\label{eq:tau_s_from_tau_c_repeat} -\end{equation} - -Accordingly, the secondary traction bounds will be written directly in terms -of the coupling torque \(\tau_c\). - -In addition, the secondary clamp force is torque-reactive. -Unlike the primary, whose axial clamping force depends only on pulley speed -and shift position, the secondary axial-force model depends explicitly on the -transmitted secondary torque. -Thus, the substitution \(\tau_s = R\tau_c\) must be made not only in the -secondary torque capacity relation, but also in the secondary axial clamp-force -model itself. - -As with the primary, the secondary bounds must be derived separately in the -sticking and slipping regimes because the inertial correction contains -\(\dot{\hat v}_b\). -In the sticking regime, we use the secondary-side sticking derivative from -\Eq{eq:vb_dot_stick_secondary}, together with the secondary rotational -equation of motion from \Eq{eq:secondary_rot_dyn}. -In the slipping regime, we instead use the regularized belt-speed evolution -law \Eq{eq:vb_relaxation}. +\subsection{Primary-Slip Branch} +\label{sec:primary-slip-branch} -% -------------------------------------------------------------- -\paragraph{Stick regime} +We first consider the branch where the primary contact is slipping while the +secondary contact remains stuck. In this case, the primary pulley no longer +enforces belt-speed compatibility. Instead, the primary contact torque is set by +the saturated kinetic traction law from \Sec{sec:kinetic-traction-law}. -Applying \Eq{eq:tau_capacity_final} with \(j=s\), and using -\(\tau_s = R\tau_c\), gives +The secondary contact, however, is still adhered. Therefore, the secondary +pulley remains kinematically compatible with the belt: +\begin{equation} +\dot\omega_s += +\frac{ +\dot v_b-\dot r_s\omega_s +}{ +r_s +}, +\label{eq:omega_s_primary_slip} +\end{equation} +Using the secondary rotational equation of motion +\Eq{eq:secondary_rotational_eom_belt}, the secondary contact torque is then \begin{equation} -|R\tau_c| -\le -r_{s,\mathrm{eff}} -\left[ -2\mu F_{\mathrm{ax},s}\tan\beta -- -\rho_b A_b \phi_s -\left( -r_{s,\mathrm{cm}}\dot{\hat v}_b +\tau_s += +\tau_{\mathrm{load}} + -\dot r_{s,\mathrm{cm}}\,\hat v_b -\right) -\right]. -\label{eq:tau_c_capacity_stick_start_secondary} +I_s +\left( +\frac{ +\dot v_b-\dot r_s\omega_s +}{ +r_s +} +\right). +\label{eq:tau_s_primary_slip} \end{equation} -Since \(R>0\), dividing by \(R\) preserves the inequality direction, so - +At the slipping primary contact, let +\[ +\sigma_p = \operatorname{sgn}(v_{\mathrm{rel},p}). +\] +Applying \Eq{eq:tau_j_slip_saturated} with \(j=p\) gives \begin{equation} -|\tau_c| -\le -\frac{r_{s,\mathrm{eff}}}{R} +\tau_p += +\sigma_p r_p \left[ -2\mu F_{\mathrm{ax},s}\tan\beta +2\mu_k F_{\mathrm{ax},p}\tan\beta - -\rho_b A_b \phi_s +\rho_b A_b \phi_p \left( -r_{s,\mathrm{cm}}\dot{\hat v}_b +r_{p,\mathrm{cm}}\dot v_b + -\dot r_{s,\mathrm{cm}}\,\hat v_b +\dot r_{p,\mathrm{cm}}v_b \right) -\right]. -\label{eq:tau_c_capacity_stick_start_secondary_divided} +\right], +\label{eq:tau_p_primary_slip} \end{equation} +where \(r_p=r_{p,\mathrm{eff}}(s)\). -In the sticking regime, -\[ -\hat v_b = r_{s,\mathrm{cm}}\omega_s, -\qquad -\dot{\hat v}_b +The remaining unknown is the belt acceleration \(\dot v_b\). This is found by +substituting \Eq{eq:tau_s_primary_slip} and \Eq{eq:tau_p_primary_slip} into the +belt equation of motion \Eq{eq:belt_transport_eom}: +\begin{equation} +m_b\dot v_b = -\dot r_{s,\mathrm{cm}}\,\omega_s -+ -r_{s,\mathrm{cm}}\,\dot\omega_s, -\] -from \Eq{eq:vb_stick_exact} and \Eq{eq:vb_dot_stick_secondary}. -Substituting these into -\Eq{eq:tau_c_capacity_stick_start_secondary_divided} gives +\frac{\tau_p}{r_p} +- +\frac{\tau_s}{r_s}. +\label{eq:belt_eom_primary_slip_start} +\end{equation} -\begin{equation} -|\tau_c| -\le -\frac{r_{s,\mathrm{eff}}}{R} +Substituting gives +\begin{align} +m_b\dot v_b +=&\; +\sigma_p \left[ -2\mu F_{\mathrm{ax},s}\tan\beta +2\mu_k F_{\mathrm{ax},p}\tan\beta - -\rho_b A_b \phi_s +\rho_b A_b \phi_p \left( -r_{s,\mathrm{cm}}^2\dot\omega_s +r_{p,\mathrm{cm}}\dot v_b + -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s +\dot r_{p,\mathrm{cm}}v_b \right) -\right]. -\label{eq:tau_c_capacity_stick_mid_secondary} -\end{equation} +\right] +\nonumber\\ +&- +\frac{\tau_{\mathrm{load}}}{r_s} +- +\frac{I_s}{r_s} +\left( +\frac{ +\dot v_b-\dot r_s\omega_s +}{ +r_s +} +\right). +\label{eq:belt_eom_primary_slip_substituted} +\end{align} + +Collecting the \(\dot v_b\) terms yields the primary-slip belt acceleration: + +\bigeq{eq:vb_dot_primary_slip}{Primary-Slip Belt Acceleration}{ +\dot v_{b,p\mathrm{-slip}} += +\frac{ +\sigma_p +\left[ +2\mu_k F_{\mathrm{ax},p}\tan\beta +- +\rho_b A_b \phi_p \dot r_{p,\mathrm{cm}}v_b +\right] +- +\dfrac{\tau_{\mathrm{load}}}{r_s} ++ +\dfrac{I_s\dot r_s\omega_s}{r_s^2} +}{ +m_b ++ +\sigma_p\rho_b A_b \phi_p r_{p,\mathrm{cm}} ++ +\dfrac{I_s}{r_s^2} +} +} + +Once \(\dot v_{b,p\mathrm{-slip}}\) is known, the branch torques follow by +substitution into \Eq{eq:tau_p_primary_slip} and +\Eq{eq:tau_s_primary_slip}. The pulley accelerations are then -Now substitute the secondary rotational dynamics from -\Eq{eq:secondary_rot_dyn}, \begin{equation} -\dot\omega_s +\dot\omega_p = -\frac{R\tau_c-\tau_{\mathrm{load}}}{I_s}, -\label{eq:omega_s_dot_sub_stick} +\frac{ +\tau_{\mathrm{eng}}-\tau_p +}{ +I_p +}, +\label{eq:omega_p_primary_slip_final} \end{equation} -and the secondary axial-force model from \Eq{eq:secondary_axial}, -using \(\tau_s = R\tau_c\) from \Eq{eq:tau_s_from_tau_c_repeat}, so that + +and \begin{equation} -F_{\mathrm{ax},s}(s,R\tau_c) +\dot\omega_s = \frac{ -R\tau_c + k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -}{2} -\frac{\dd \theta_s}{\dd s} -+ -k_{s,x}(x_{s,0}+s). -\label{eq:Fax_s_sub_stick_tau_c} +\dot v_{b,p\mathrm{-slip}}-\dot r_s\omega_s +}{ +r_s +}. +\label{eq:omega_s_primary_slip_final} \end{equation} -This yields +Thus, in the primary-slip branch, the primary contact torque is set by kinetic +traction, the secondary contact still enforces belt-speed compatibility, and +the belt equation of motion determines the resulting belt acceleration. This +branch remains valid only while the secondary contact can remain adhered. If the +secondary contact also fails its sticking condition, the model must transition +to the two-contact slip branch. +\subsection{Secondary-Slip Branch} +\label{sec:secondary-slip-branch} + +We next consider the branch where the secondary contact is slipping while the +primary contact remains stuck. In this case, the secondary contact torque is set +by saturated kinetic traction, while the primary contact still enforces +belt-speed compatibility. + +The primary contact remains adhered, so the primary pulley must satisfy \begin{equation} -\begin{aligned} -|\tau_c| -\le\;& -\frac{r_{s,\mathrm{eff}}}{R} -\Biggl[ -\mu\tan\beta +\dot\omega_p += +\frac{ +\dot v_b-\dot r_p\omega_p +}{ +r_p +}, +\label{eq:omega_p_secondary_slip} +\end{equation} +where \(r_p=r_{p,\mathrm{eff}}(s)\). Using the primary rotational equation of +motion \Eq{eq:primary_rotational_eom_belt}, the primary contact torque is +therefore +\begin{equation} +\tau_p += +\tau_{\mathrm{eng}} +- +I_p \left( -R\tau_c -+ -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\right) -\frac{\dd \theta_s}{\dd s} -\\[4pt] -&\qquad -+ -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) +\frac{ +\dot v_b-\dot r_p\omega_p +}{ +r_p +} +\right). +\label{eq:tau_p_secondary_slip} +\end{equation} + +At the slipping secondary contact, let +\[ +\sigma_s = \operatorname{sgn}(v_{\mathrm{rel},s}). +\] +Applying \Eq{eq:tau_j_slip_saturated} with \(j=s\) gives +\begin{equation} +\tau_s += +\sigma_s r_s +\left[ +2\mu_k F_{\mathrm{ax},s}(s,\tau_s)\tan\beta - \rho_b A_b \phi_s \left( -\frac{r_{s,\mathrm{cm}}^2}{I_s}(R\tau_c-\tau_{\mathrm{load}}) +r_{s,\mathrm{cm}}\dot v_b + -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s +\dot r_{s,\mathrm{cm}}v_b \right) -\Biggr]. -\end{aligned} -\label{eq:tau_c_capacity_stick_explicit_secondary} +\right], +\label{eq:tau_s_secondary_slip_start} \end{equation} +where \(r_s=r_{s,\mathrm{eff}}(s)\). -The coupling torque now appears through both the torque-reactive clamp force -and the secondary angular acceleration, so the positive and negative -directions must be treated separately. - -\paragraph{Positive-direction sticking bound \(\tau_c \ge 0\)} - -For \(\tau_c \ge 0\), \(|\tau_c|=\tau_c\), so -\Eq{eq:tau_c_capacity_stick_explicit_secondary} becomes - +Unlike the primary slip branch, this expression is implicit in \(\tau_s\) +because the secondary axial force is torque-reactive. Substituting the secondary +axial force model from \Eq{eq:secondary_axial} gives \begin{equation} \begin{aligned} -\tau_c -\le\;& -\frac{r_{s,\mathrm{eff}}}{R} +\tau_s += +\sigma_s r_s \Biggl[ -\mu\tan\beta +&\mu_k\tan\beta \left( -R\tau_c +\tau_s + k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) \right) -\frac{\dd \theta_s}{\dd s} -\\[4pt] -&\qquad -+ -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) +\frac{\dd\theta_s}{\dd s} +\\ +&+ +2\mu_k\tan\beta\,k_{s,x}(x_{s,0}+s) - \rho_b A_b \phi_s \left( -\frac{r_{s,\mathrm{cm}}^2}{I_s}(R\tau_c-\tau_{\mathrm{load}}) +r_{s,\mathrm{cm}}\dot v_b + -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s +\dot r_{s,\mathrm{cm}}v_b \right) \Biggr]. \end{aligned} -\label{eq:tau_c_plus_stick_expand_secondary} +\label{eq:tau_s_secondary_slip_implicit} \end{equation} -Collecting the \(\tau_c\) terms on the left gives - -\begin{align} -\Bigl( -1 -- -r_{s,\mathrm{eff}}\mu\tan\beta \frac{\dd\theta_s}{\dd s} -+ -r_{s,\mathrm{eff}}\rho_b A_b \phi_s \frac{r_{s,\mathrm{cm}}^2}{I_s} -\Bigr)\tau_c -\;\le\;& -\frac{r_{s,\mathrm{eff}}}{R} -\mu\tan\beta -\frac{\dd\theta_s}{\dd s}\, -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\nonumber\\ -&+ -\frac{r_{s,\mathrm{eff}}}{R} -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -\nonumber\\ -&+ -\frac{r_{s,\mathrm{eff}}}{R} -\rho_b A_b \phi_s -\left( -\frac{r_{s,\mathrm{cm}}^2}{I_s}\tau_{\mathrm{load}} -- -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s -\right). -\label{eq:tau_c_plus_stick_rearrange_secondary} -\end{align} - -Hence, +Collecting the \(\tau_s\) terms gives \begin{equation} -\tau_c \le \tau_{c,s,+}^{\mathrm{stick}}, -\label{eq:tau_c_plus_stick_ineq_secondary} -\end{equation} -with - -\bigeq{eq:tau_s_plus_stick}{Secondary Stick-Branch Upper Bound}{ -\tau_{c,s,+}^{\mathrm{stick}} +\begin{aligned} +\tau_s = \frac{ -\dfrac{r_{s,\mathrm{eff}}}{R} -\Bigl[ -\mu\tan\beta -\dfrac{\dd\theta_s}{\dd s}\, +\sigma_s r_s +\Biggl[ +\mu_k\tan\beta\, k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\dfrac{\dd\theta_s}{\dd s} + -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -+ +2\mu_k\tan\beta\,k_{s,x}(x_{s,0}+s) +- \rho_b A_b \phi_s \left( -\dfrac{r_{s,\mathrm{cm}}^2}{I_s}\tau_{\mathrm{load}} -- -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s +r_{s,\mathrm{cm}}\dot v_b ++ +\dot r_{s,\mathrm{cm}}v_b \right) -\Bigr] +\Biggr] }{ 1 - -r_{s,\mathrm{eff}}\mu\tan\beta \dfrac{\dd\theta_s}{\dd s} -+ -r_{s,\mathrm{eff}}\rho_b A_b \phi_s \dfrac{r_{s,\mathrm{cm}}^2}{I_s} +\sigma_s r_s\mu_k\tan\beta +\dfrac{\dd\theta_s}{\dd s} }. -} - -\paragraph{Negative-direction sticking bound \(\tau_c \le 0\)} +\end{aligned} +\label{eq:tau_s_secondary_slip} +\end{equation} -For \(\tau_c \le 0\), \(|\tau_c|=-\tau_c\), so -\Eq{eq:tau_c_capacity_stick_explicit_secondary} becomes +The remaining unknown is the belt acceleration \(\dot v_b\). Substituting +\Eq{eq:tau_p_secondary_slip} and \Eq{eq:tau_s_secondary_slip} into the belt +equation of motion \Eq{eq:belt_transport_eom}, +\[ +m_b\dot v_b += +\frac{\tau_p}{r_p} +- +\frac{\tau_s}{r_s}, +\] +and collecting the \(\dot v_b\) terms gives the secondary-slip belt +acceleration: -\begin{equation} -\begin{aligned} --\tau_c -\le\;& -\frac{r_{s,\mathrm{eff}}}{R} -\Biggl[ -\mu\tan\beta -\left( -R\tau_c +\widebigeq{eq:vb_dot_secondary_slip}{Secondary-Slip Belt Acceleration}{ +\dot v_{b,s\mathrm{-slip}} += +\frac{ +\dfrac{\tau_{\mathrm{eng}}}{r_p} + +\dfrac{I_p\dot r_p\omega_p}{r_p^2} +- +\dfrac{ +\sigma_s +\left[ +\mu_k\tan\beta\, k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\right) -\frac{\dd \theta_s}{\dd s} -\\[4pt] -&\qquad +\dfrac{\dd\theta_s}{\dd s} + -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) +2\mu_k\tan\beta\,k_{s,x}(x_{s,0}+s) - -\rho_b A_b \phi_s -\left( -\frac{r_{s,\mathrm{cm}}^2}{I_s}(R\tau_c-\tau_{\mathrm{load}}) -+ -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s -\right) -\Biggr]. -\end{aligned} -\label{eq:tau_c_minus_stick_expand_secondary} -\end{equation} - -Collecting the \(\tau_c\) terms on the left gives - -\begin{align} -\Bigl( +\rho_b A_b \phi_s \dot r_{s,\mathrm{cm}}v_b +\right] +}{ 1 +- +\sigma_s r_s\mu_k\tan\beta +\dfrac{\dd\theta_s}{\dd s} +} +}{ +m_b + -r_{s,\mathrm{eff}}\mu\tan\beta \frac{\dd\theta_s}{\dd s} +\dfrac{I_p}{r_p^2} - -r_{s,\mathrm{eff}}\rho_b A_b \phi_s \frac{r_{s,\mathrm{cm}}^2}{I_s} -\Bigr)\tau_c -\;\ge\;& --\frac{r_{s,\mathrm{eff}}}{R} -\mu\tan\beta -\frac{\dd\theta_s}{\dd s}\, -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\nonumber\\ -&- -\frac{r_{s,\mathrm{eff}}}{R} -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -\nonumber\\ -&- -\frac{r_{s,\mathrm{eff}}}{R} -\rho_b A_b \phi_s -\left( -\frac{r_{s,\mathrm{cm}}^2}{I_s}\tau_{\mathrm{load}} +\dfrac{ +\sigma_s\rho_b A_b \phi_s r_{s,\mathrm{cm}} +}{ +1 - -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s -\right). -\label{eq:tau_c_minus_stick_rearrange_secondary} -\end{align} +\sigma_s r_s\mu_k\tan\beta +\dfrac{\dd\theta_s}{\dd s} +} +} +} + +Once \(\dot v_{b,s\mathrm{-slip}}\) is known, the branch torques follow by +substitution into \Eq{eq:tau_p_secondary_slip} and +\Eq{eq:tau_s_secondary_slip}. The pulley accelerations are then -Hence, \begin{equation} -\tau_c \ge \tau_{c,s,-}^{\mathrm{stick}}, -\label{eq:tau_c_minus_stick_ineq_secondary} +\dot\omega_p += +\frac{ +\dot v_{b,s\mathrm{-slip}}-\dot r_p\omega_p +}{ +r_p +}, +\label{eq:omega_p_secondary_slip_final} \end{equation} -with -\bigeq{eq:tau_s_minus_stick}{Secondary Stick-Branch Lower Bound}{ -\tau_{c,s,-}^{\mathrm{stick}} +and + +\begin{equation} +\dot\omega_s = --\frac{ -\dfrac{r_{s,\mathrm{eff}}}{R} -\Bigl[ -\mu\tan\beta -\dfrac{\dd\theta_s}{\dd s}\, -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -+ -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -+ -\rho_b A_b \phi_s -\left( -\dfrac{r_{s,\mathrm{cm}}^2}{I_s}\tau_{\mathrm{load}} -- -2r_{s,\mathrm{cm}}\dot r_{s,\mathrm{cm}}\,\omega_s -\right) -\Bigr] +\frac{ +\tau_s-\tau_{\mathrm{load}} }{ -1 -+ -r_{s,\mathrm{eff}}\mu\tan\beta \dfrac{\dd\theta_s}{\dd s} -- -r_{s,\mathrm{eff}}\rho_b A_b \phi_s \dfrac{r_{s,\mathrm{cm}}^2}{I_s} +I_s }. -} - -Therefore, in the sticking regime the secondary pulley contributes the interval -\begin{equation} -\boxed{ -\tau_{c,s,-}^{\mathrm{stick}} -\le -\tau_c -\le -\tau_{c,s,+}^{\mathrm{stick}}. -} -\label{eq:secondary_tau_interval_stick} +\label{eq:omega_s_secondary_slip_final} \end{equation} -% -------------------------------------------------------------- -\paragraph{Slip regime} +Thus, in the secondary-slip branch, the secondary contact torque is set by +kinetic traction, including the torque-reactive helix feedback, while the +primary contact still enforces belt-speed compatibility. This branch remains +valid only while the primary contact can remain adhered. If the primary contact +also fails its sticking condition, the model must transition to the two-contact +slip branch. + +\subsection{Two-Contact Slip Branch} +\label{sec:two-contact-slip-branch} -In the slipping regime, the belt-speed derivative is given directly by the -regularized closure \Eq{eq:vb_relaxation}: +Finally, consider the branch where both belt--pulley contacts are slipping. +In this case, neither pulley enforces belt-speed compatibility. Both contact +torques are instead determined by saturated kinetic traction, and the belt +acceleration is obtained directly from the belt equation of motion. + +Let \[ -\dot{\hat v}_b -= -\frac{v_b^\ast-\hat v_b}{T_b}. +\sigma_p = \operatorname{sgn}(v_{\mathrm{rel},p}), +\qquad +\sigma_s = \operatorname{sgn}(v_{\mathrm{rel},s}). \] -Applying \Eq{eq:tau_capacity_final} with \(j=s\), and again using -\(\tau_s = R\tau_c\) from \Eq{eq:tau_s_from_tau_c_repeat}, gives +At the primary contact, applying \Eq{eq:tau_j_slip_saturated} with \(j=p\) +gives \begin{equation} -|R\tau_c| -\le -r_{s,\mathrm{eff}} +\tau_p += +\sigma_p r_p \left[ -2\mu F_{\mathrm{ax},s}\tan\beta +2\mu_k F_{\mathrm{ax},p}\tan\beta - -\rho_b A_b \phi_s +\rho_b A_b \phi_p \left( -r_{s,\mathrm{cm}}\frac{v_b^\ast-\hat v_b}{T_b} +r_{p,\mathrm{cm}}\dot v_b + -\dot r_{s,\mathrm{cm}}\,\hat v_b +\dot r_{p,\mathrm{cm}}v_b \right) -\right]. -\label{eq:tau_c_capacity_slip_start_secondary} +\right], +\label{eq:tau_p_two_slip} \end{equation} -Dividing by \(R>0\) gives +where \(r_p=r_{p,\mathrm{eff}}(s)\). + +At the secondary contact, applying \Eq{eq:tau_j_slip_saturated} with \(j=s\) +gives \begin{equation} -|\tau_c| -\le -\frac{r_{s,\mathrm{eff}}}{R} +\tau_s += +\sigma_s r_s \left[ -2\mu F_{\mathrm{ax},s}\tan\beta +2\mu_k F_{\mathrm{ax},s}(s,\tau_s)\tan\beta - \rho_b A_b \phi_s \left( -r_{s,\mathrm{cm}}\frac{v_b^\ast-\hat v_b}{T_b} +r_{s,\mathrm{cm}}\dot v_b + -\dot r_{s,\mathrm{cm}}\,\hat v_b +\dot r_{s,\mathrm{cm}}v_b \right) -\right]. -\label{eq:tau_c_capacity_slip_start_secondary_divided} +\right], +\label{eq:tau_s_two_slip_start} \end{equation} -Using the secondary axial-force model \Eq{eq:secondary_axial}, with -\(\tau_s = R\tau_c\) substituted through \Eq{eq:tau_s_from_tau_c_repeat}, gives +where \(r_s=r_{s,\mathrm{eff}}(s)\). As in \Sec{sec:secondary-slip-branch}, the +secondary expression is implicit because the secondary axial force is +torque-reactive. Substituting \Eq{eq:secondary_axial} and collecting the +\(\tau_s\) terms gives \begin{equation} -F_{\mathrm{ax},s}(s,R\tau_c) +\begin{aligned} +\tau_s = \frac{ -R\tau_c + k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -}{2} -\frac{\dd \theta_s}{\dd s} -+ -k_{s,x}(x_{s,0}+s). -\label{eq:Fax_s_sub_slip_tau_c} -\end{equation} - -Substituting this into -\Eq{eq:tau_c_capacity_slip_start_secondary_divided} yields - -\begin{equation} -\begin{aligned} -|\tau_c| -\le\;& -\frac{r_{s,\mathrm{eff}}}{R} +\sigma_s r_s \Biggl[ -\mu\tan\beta -\left( -R\tau_c -+ +\mu_k\tan\beta\, k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\right) -\frac{\dd \theta_s}{\dd s} -\\[4pt] -&\qquad +\dfrac{\dd\theta_s}{\dd s} + -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) +2\mu_k\tan\beta\,k_{s,x}(x_{s,0}+s) - \rho_b A_b \phi_s \left( -r_{s,\mathrm{cm}}\frac{v_b^\ast-\hat v_b}{T_b} +r_{s,\mathrm{cm}}\dot v_b + -\dot r_{s,\mathrm{cm}}\,\hat v_b +\dot r_{s,\mathrm{cm}}v_b \right) -\Biggr]. +\Biggr] +}{ +1 +- +\sigma_s r_s\mu_k\tan\beta +\dfrac{\dd\theta_s}{\dd s} +}. \end{aligned} -\label{eq:tau_c_capacity_slip_explicit_secondary} +\label{eq:tau_s_two_slip} \end{equation} -In contrast to the sticking case, the inertial correction no longer depends on -\(\tau_c\). -However, the secondary bound remains implicit because the torque-reactive clamp -force still depends on \(R\tau_c\). +The belt equation of motion is still -\paragraph{Positive-direction slipping bound \(\tau_c \ge 0\)} - -For \(\tau_c \ge 0\), \(|\tau_c|=\tau_c\). -Collecting the \(\tau_c\) terms on the left gives - -\begin{align} -\Bigl( -1 -- -r_{s,\mathrm{eff}}\mu\tan\beta \frac{\dd\theta_s}{\dd s} -\Bigr)\tau_c -\;\le\;& -\frac{r_{s,\mathrm{eff}}}{R} -\mu\tan\beta -\frac{\dd\theta_s}{\dd s}\, -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\nonumber\\ -&+ -\frac{r_{s,\mathrm{eff}}}{R} -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -\nonumber\\ -&- -\frac{r_{s,\mathrm{eff}}}{R} -\rho_b A_b \phi_s -\left( -r_{s,\mathrm{cm}}\frac{v_b^\ast-\hat v_b}{T_b} -+ -\dot r_{s,\mathrm{cm}}\,\hat v_b -\right). -\label{eq:tau_c_plus_slip_rearrange_secondary} -\end{align} +\[ +m_b\dot v_b += +\frac{\tau_p}{r_p} +- +\frac{\tau_s}{r_s}. +\] -Hence, -\begin{equation} -\tau_c \le \tau_{c,s,+}^{\mathrm{slip}}, -\label{eq:tau_c_plus_slip_ineq_secondary} -\end{equation} -with +Substituting \Eq{eq:tau_p_two_slip} and \Eq{eq:tau_s_two_slip}, then collecting +the \(\dot v_b\) terms, gives the two-contact-slip belt acceleration: -\bigeq{eq:tau_s_plus_slip}{Secondary Slip-Branch Upper Bound}{ -\tau_{c,s,+}^{\mathrm{slip}} +\widebigeq{eq:vb_dot_two_slip}{Two-Contact-Slip Belt Acceleration}{ +\dot v_{b,ps\mathrm{-slip}} = \frac{ -\dfrac{r_{s,\mathrm{eff}}}{R} -\Bigl[ -\mu\tan\beta -\dfrac{\dd\theta_s}{\dd s}\, +\sigma_p +\left[ +2\mu_k F_{\mathrm{ax},p}\tan\beta +- +\rho_b A_b \phi_p \dot r_{p,\mathrm{cm}}v_b +\right] +- +\dfrac{ +\sigma_s +\left[ +\mu_k\tan\beta\, k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) +\dfrac{\dd\theta_s}{\dd s} + -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) +2\mu_k\tan\beta\,k_{s,x}(x_{s,0}+s) - -\rho_b A_b \phi_s -\left( -r_{s,\mathrm{cm}}\dfrac{v_b^\ast-\hat v_b}{T_b} +\rho_b A_b \phi_s \dot r_{s,\mathrm{cm}}v_b +\right] +}{ +1 +- +\sigma_s r_s\mu_k\tan\beta +\dfrac{\dd\theta_s}{\dd s} +} +}{ +m_b + -\dot r_{s,\mathrm{cm}}\,\hat v_b -\right) -\Bigr] +\sigma_p\rho_b A_b \phi_p r_{p,\mathrm{cm}} +- +\dfrac{ +\sigma_s\rho_b A_b \phi_s r_{s,\mathrm{cm}} }{ 1 - -r_{s,\mathrm{eff}}\mu\tan\beta \dfrac{\dd\theta_s}{\dd s} -}. +\sigma_s r_s\mu_k\tan\beta +\dfrac{\dd\theta_s}{\dd s} +} +} } -\paragraph{Negative-direction slipping bound \(\tau_c \le 0\)} - -For \(\tau_c \le 0\), \(|\tau_c|=-\tau_c\). -Collecting the \(\tau_c\) terms on the left gives - -\begin{align} -\Bigl( -1 -+ -r_{s,\mathrm{eff}}\mu\tan\beta \frac{\dd\theta_s}{\dd s} -\Bigr)\tau_c -\;\ge\;& --\frac{r_{s,\mathrm{eff}}}{R} -\mu\tan\beta -\frac{\dd\theta_s}{\dd s}\, -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -\nonumber\\ -&- -\frac{r_{s,\mathrm{eff}}}{R} -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -\nonumber\\ -&+ -\frac{r_{s,\mathrm{eff}}}{R} -\rho_b A_b \phi_s -\left( -r_{s,\mathrm{cm}}\frac{v_b^\ast-\hat v_b}{T_b} -+ -\dot r_{s,\mathrm{cm}}\,\hat v_b -\right). -\label{eq:tau_c_minus_slip_rearrange_secondary} -\end{align} +Once \(\dot v_{b,ps\mathrm{-slip}}\) is known, the branch torques follow by +substitution into \Eq{eq:tau_p_two_slip} and \Eq{eq:tau_s_two_slip}. Since +neither contact is adhered, the pulley accelerations are obtained directly from +the rotational equations of motion: -Hence, \begin{equation} -\tau_c \ge \tau_{c,s,-}^{\mathrm{slip}}, -\label{eq:tau_c_minus_slip_ineq_secondary} +\dot\omega_p += +\frac{ +\tau_{\mathrm{eng}}-\tau_p +}{ +I_p +}, +\label{eq:omega_p_two_slip_final} \end{equation} -with -\bigeq{eq:tau_s_minus_slip}{Secondary Slip-Branch Lower Bound}{ -\tau_{c,s,-}^{\mathrm{slip}} +and + +\begin{equation} +\dot\omega_s = --\frac{ -\dfrac{r_{s,\mathrm{eff}}}{R} -\Bigl[ -\mu\tan\beta -\dfrac{\dd\theta_s}{\dd s}\, -k_{s,\theta}\!\bigl(\theta_{s,0}+\theta_s(s)\bigr) -+ -2\mu\tan\beta\,k_{s,x}(x_{s,0}+s) -- -\rho_b A_b \phi_s -\left( -r_{s,\mathrm{cm}}\dfrac{v_b^\ast-\hat v_b}{T_b} -+ -\dot r_{s,\mathrm{cm}}\,\hat v_b -\right) -\Bigr] +\frac{ +\tau_s-\tau_{\mathrm{load}} }{ -1 -+ -r_{s,\mathrm{eff}}\mu\tan\beta \dfrac{\dd\theta_s}{\dd s} +I_s }. -} +\label{eq:omega_s_two_slip_final} +\end{equation} -Therefore, in the slipping regime the secondary pulley contributes the interval -\begin{equation} -\boxed{ -\tau_{c,s,-}^{\mathrm{slip}} +Thus, in the two-contact slip branch, both pulley contacts are governed by +kinetic traction, and the belt is not kinematically constrained to either pulley. +The belt acceleration is determined only by the balance of the two saturated +contact torques through the belt equation of motion. + +\subsection{Contact-State Selection} +\label{sec:contact-state-selection} + +The preceding subsections define the possible torque-transfer closures. The +active branch is selected by checking which contacts can remain adhered. + +Let the primary sticking condition be +\[ +\mathcal S_p +\equiv +\left( +|v_{\mathrm{rel},p}| \le \varepsilon_v +\right) +\land +\left( +|\tau_{p,\mathrm{ns}}| \le -\tau_c +\tau_{p,\max}^{\mathrm{stick}} +\right), +\] +and let the secondary sticking condition be +\[ +\mathcal S_s +\equiv +\left( +|v_{\mathrm{rel},s}| \le \varepsilon_v +\right) +\land +\left( +\tau_{s,-}^{\mathrm{stick}} \le -\tau_{c,s,+}^{\mathrm{slip}}. -} -\label{eq:secondary_tau_interval_slip} +\tau_{s,\mathrm{ns}} +\le +\tau_{s,+}^{\mathrm{stick}} +\right). +\] + +The branch closure for the belt acceleration and contact torques is then + +\begin{equation} +(\dot v_b,\tau_p,\tau_s) += +\begin{cases} +\left( +\dot v_{b,\mathrm{ns}}, +\tau_{p,\mathrm{ns}}, +\tau_{s,\mathrm{ns}} +\right), +& +\mathcal S_p \land \mathcal S_s, +\\[8pt] +\left( +\dot v_{b,p\mathrm{-slip}}, +\tau_p \text{ from } \Eq{eq:tau_p_primary_slip}, +\tau_s \text{ from } \Eq{eq:tau_s_primary_slip} +\right), +& +\neg\mathcal S_p \land \mathcal S_s, +\\[8pt] +\left( +\dot v_{b,s\mathrm{-slip}}, +\tau_p \text{ from } \Eq{eq:tau_p_secondary_slip}, +\tau_s \text{ from } \Eq{eq:tau_s_secondary_slip} +\right), +& +\mathcal S_p \land \neg\mathcal S_s, +\\[8pt] +\left( +\dot v_{b,ps\mathrm{-slip}}, +\tau_p \text{ from } \Eq{eq:tau_p_two_slip}, +\tau_s \text{ from } \Eq{eq:tau_s_two_slip} +\right), +& +\neg\mathcal S_p \land \neg\mathcal S_s. +\end{cases} +\label{eq:branch_closure_law} \end{equation} +The corresponding belt accelerations are given by +\Eq{eq:no_slip_vb_dot}, \Eq{eq:vb_dot_primary_slip}, +\Eq{eq:vb_dot_secondary_slip}, and \Eq{eq:vb_dot_two_slip}, +respectively. -\subsection{Section Conclusion and Transition} -\label{sec:traction_conclusion} +Thus, when both contacts can remain adhered, the model uses the no-slip torque +demands. When one or both contacts cannot remain adhered, the corresponding slip +branch replaces the no-slip demand with saturated kinetic traction and solves +the belt acceleration from the belt equation of motion. -This section derived the traction-capacity model of the CVT from the local -belt--pulley force balance and converted it into explicit torque bounds for the -primary and secondary pulleys. +\subsection{Section Summary} +\label{sec:slip-branch-summary} -With these traction limits now established, the torque-transfer model is fully -closed. The transmitted coupling torque is no longer just the value required by -the no-slip dynamics; it is now constrained by the admissible belt traction, -which determines whether the transmission remains in stick or saturates onto a -slipping branch. +This section completed the torque-transfer closure for cases where the no-slip +candidate cannot be maintained. Relative contact speeds were introduced to +identify whether each belt--pulley interface is adhered or sliding, and kinetic +traction was used to define the torque applied by a slipping contact. +The resulting branch structure separates the possible contact states into +no-slip, primary slip, secondary slip, and two-contact slip. In each slipping +case, the saturated contact torque replaces the no-slip demand, and the belt +equation of motion determines the corresponding belt acceleration. + +Together with the no-slip demand from \Sec{sec:no-slip-belt-dynamics} and the +traction admissibility conditions from \Sec{sec:traction-limits}, the branch +selection law in \Eq{eq:branch_closure_law} provides the final torque-transfer +rule needed by the complete CVT dynamic model. -% ============================================================== \section{Complete CVT Dynamic Model} \label{sec:complete_model} This section collects the results of the preceding derivations into one closed -nonlinear dynamical model. -No new physics is introduced here. -The purpose is simply to summarize, in one place, +nonlinear dynamical model. No new physics is introduced here. The purpose is to +summarize, in one place, \begin{enumerate} \item the state variables, - \item the geometry and belt-kinematic quantities computed from those states, - \item the torque and regime closures, + \item the geometric and contact quantities computed from those states, + \item the torque-transfer closure, \item and the final governing equations of motion. \end{enumerate} @@ -7328,7 +6976,7 @@ \subsection{State Definition} \omega_s,\; s,\; \dot s,\; -\hat v_b +v_b \big), \label{eq:state_vector_complete} \end{equation} @@ -7338,16 +6986,15 @@ \subsection{State Definition} \item \(\omega_s\) is the secondary pulley angular velocity, \item \(s\) is the axial shift coordinate, \item \(\dot s\) is the axial shift velocity, - \item \(\hat v_b\) is the modeled belt transport speed. + \item \(v_b\) is the belt transport speed. \end{itemize} % -------------------------------------------------------------- -\subsection{Geometry and Belt Kinematics} +\subsection{Geometry and Contact Kinematics} At each instant, the shift coordinate \(s\) determines the pulley geometry through the belt-length constraint and the conical kinematic relations derived -earlier. -In particular, +in \Sec{sec:geometry}. In particular, \[ r_{p,\mathrm{eff}},\quad r_{s,\mathrm{eff}},\quad @@ -7356,7 +7003,14 @@ \subsection{Geometry and Belt Kinematics} \phi_p,\quad \phi_s \] -are all functions of \(s\), and the geometric CVT ratio is +are functions of \(s\). For compactness in the complete model, write +\[ +r_p = r_{p,\mathrm{eff}}(s), +\qquad +r_s = r_{s,\mathrm{eff}}(s). +\] + +The geometric CVT ratio remains \begin{equation} R(s) = @@ -7371,253 +7025,143 @@ \subsection{Geometry and Belt Kinematics} \end{equation} as given by \Eq{eq:Rdot_final}. -The pulley-imposed belt-line speeds from \Sec{sec:belt_transport_motion} are +The relative contact speeds at the two pulley interfaces are \begin{equation} -v_{p,\mathrm{cm}} +v_{\mathrm{rel},p} = -r_{p,\mathrm{cm}}(s)\,\omega_p, +r_p\omega_p - v_b, \qquad -v_{s,\mathrm{cm}} +v_{\mathrm{rel},s} = -r_{s,\mathrm{cm}}(s)\,\omega_s. -\label{eq:belt_line_speeds_complete} +v_b - r_s\omega_s, +\label{eq:v_rel_complete} \end{equation} +as defined in \Eq{eq:v_rel_primary} and \Eq{eq:v_rel_secondary}. These +quantities determine whether each contact is eligible to remain adhered or must +be treated as sliding. + +% -------------------------------------------------------------- +\subsection{Torque-Transfer Closure} + +The torque-transfer subsystem is closed by determining the primary contact +torque \(\tau_p\), the secondary contact torque \(\tau_s\), and the belt +acceleration \(\dot v_b\). These quantities are computed algebraically from the +current state and contact branch. + +The no-slip candidate derived in \Sec{sec:no-slip-torque-demand} provides +\[ +\dot v_{b,\mathrm{ns}}, +\qquad +\tau_{p,\mathrm{ns}}, +\qquad +\tau_{s,\mathrm{ns}}, +\] +from \Eq{eq:no_slip_vb_dot}, \Eq{eq:tau_p_ns}, and \Eq{eq:tau_s_ns}. These are +the belt acceleration and contact torque demands required for adhered motion at +both pulleys. -The regularized target belt speed is +The admissibility conditions derived in \Sec{sec:primary_tau_limits} and +\Sec{sec:secondary_tau_limits} determine whether those no-slip demands can be +supported. The primary contact is admissible when \begin{equation} -v_b^\ast -= -\frac{1}{2}\left(v_{p,\mathrm{cm}}+v_{s,\mathrm{cm}}\right). -\label{eq:vb_target_complete} +\left|\tau_{p,\mathrm{ns}}\right| +\le +\tau_{p,\max}^{\mathrm{stick}}, +\label{eq:primary_stick_condition_complete} \end{equation} - -In the sticking regime, the belt speed is imposed exactly by the adhered -kinematics: +as in \Eq{eq:primary_no_slip_admissibility}. The secondary contact is admissible +when \begin{equation} -\hat v_b = v_{p,\mathrm{cm}} = v_{s,\mathrm{cm}}. -\label{eq:vb_stick_complete} +\tau_{s,-}^{\mathrm{stick}} +\le +\tau_{s,\mathrm{ns}} +\le +\tau_{s,+}^{\mathrm{stick}}, +\label{eq:secondary_stick_condition_complete} \end{equation} +as in \Eq{eq:secondary_no_slip_admissibility}. -The corresponding sticking derivative is obtained by differentiating this -compatibility relation, for example from the primary side, +The active contact branch is selected using the closure law from +\Sec{sec:contact-state-selection}. In compact form, \begin{equation} -\dot{\hat v}_{b,\mathrm{stick}} +(\dot v_b,\tau_p,\tau_s) = -\dot r_{p,\mathrm{cm}}\,\omega_p -+ -r_{p,\mathrm{cm}}\,\dot\omega_p, -\label{eq:vb_dot_stick_complete} +\text{the branch output selected by \Eq{eq:branch_closure_law}}. +\label{eq:branch_closure_complete} \end{equation} -with an equivalent expression obtained from the secondary side under sticking -compatibility. -In the slipping regime, the belt-speed derivative is given by the regularized -law -\begin{equation} -\dot{\hat v}_{b,\mathrm{slip}} +If both contacts remain adhered, the branch output is +\[ +(\dot v_b,\tau_p,\tau_s) = -\frac{v_b^\ast-\hat v_b}{T_b}. -\label{eq:vb_dot_slip_complete} -\end{equation} +(\dot v_{b,\mathrm{ns}},\tau_{p,\mathrm{ns}},\tau_{s,\mathrm{ns}}). +\] +If one or both contacts slide, the corresponding slip branch replaces the +no-slip demand with saturated kinetic traction and solves \(\dot v_b\) from the +belt equation of motion. The primary-slip, secondary-slip, and two-contact-slip +belt accelerations are given by \Eq{eq:vb_dot_primary_slip}, +\Eq{eq:vb_dot_secondary_slip}, and \Eq{eq:vb_dot_two_slip}, respectively. % -------------------------------------------------------------- -\subsection{Torque and Regime Closure} - -The internal torque transmitted through the CVT is the coupling torque -\(\tau_c\). - -\paragraph{No-slip torque demand.} +\subsection{Governing ODEs} -If sticking is maintained, the required coupling torque is the no-slip torque -derived in \Eq{eq:tau_ns_final}: +Once the branch closure has supplied \(\dot v_b\), \(\tau_p\), and \(\tau_s\), +the primary pulley equation of motion is \begin{equation} -\tau_{\mathrm{ns}} +\dot\omega_p = \frac{ -\tau_{\mathrm{eng}} -+ -\dfrac{I_p}{I_s}R\,\tau_{\mathrm{load}} -- -I_p\,\omega_s\,\dot R +\tau_{\mathrm{eng}}(\omega_p)-\tau_p }{ -1+\dfrac{I_p}{I_s}R^2 +I_p }. -\label{eq:tau_ns_complete} +\label{eq:omega_p_dot_complete} \end{equation} -\paragraph{Stick and slip traction intervals.} - -The branchwise primary and secondary traction bounds derived in -\Sec{sec:primary_tau_limits} and \Sec{sec:secondary_tau_limits} -combine into a sticking admissible interval -\begin{equation} -\tau_-^{\mathrm{stick}} -\le -\tau_c -\le -\tau_+^{\mathrm{stick}}, -\label{eq:tau_pm_stick_complete} -\end{equation} -with -\begin{equation} -\tau_-^{\mathrm{stick}} -= -\max\!\left( -\tau_{c,p,-}^{\mathrm{stick}}, -\tau_{c,s,-}^{\mathrm{stick}} -\right), -\qquad -\tau_+^{\mathrm{stick}} -= -\min\!\left( -\tau_{c,p,+}^{\mathrm{stick}}, -\tau_{c,s,+}^{\mathrm{stick}} -\right), -\label{eq:tau_pm_stick_complete_explicit} -\end{equation} -and a slipping admissible interval -\begin{equation} -\tau_-^{\mathrm{slip}} -\le -\tau_c -\le -\tau_+^{\mathrm{slip}}, -\label{eq:tau_pm_slip_complete} -\end{equation} -with +The secondary pulley equation of motion is \begin{equation} -\tau_-^{\mathrm{slip}} -= -\max\!\left( -\tau_{c,p,-}^{\mathrm{slip}}, -\tau_{c,s,-}^{\mathrm{slip}} -\right), -\qquad -\tau_+^{\mathrm{slip}} +\dot\omega_s = -\min\!\left( -\tau_{c,p,+}^{\mathrm{slip}}, -\tau_{c,s,+}^{\mathrm{slip}} -\right). -\label{eq:tau_pm_slip_complete_explicit} +\frac{ +\tau_s-\tau_{\mathrm{load}}(\omega_s) +}{ +I_s +}. +\label{eq:omega_s_dot_complete} \end{equation} -\paragraph{Slip metric.} - -The slip metric from \Eq{eq:v_delta_def} is +The shift-position kinematics are \begin{equation} -v_{\Delta} +\frac{\dd}{\dd t}s = -r_{p,\mathrm{eff}}\,\omega_p -- -r_{s,\mathrm{eff}}\,\omega_s. -\label{eq:v_delta_complete} -\end{equation} - -\paragraph{Regime selection.} - -Using \(\tau_{\mathrm{ns}}\), the sticking interval, the slipping interval, -and \(v_\Delta\), the active transmission regime selects both the actual -coupling torque and the appropriate belt-speed derivative. - -The coupling torque is -\begin{equation} -\tau_c = -\begin{cases} -\tau_{\mathrm{ns}}, -& -v_{\Delta}=0 -\;\text{and}\; -\tau_-^{\mathrm{stick}} \le \tau_{\mathrm{ns}} \le \tau_+^{\mathrm{stick}}, -\\[10pt] -\tau_+^{\mathrm{slip}}, -& -v_{\Delta}>0 -\;\text{or}\; -\bigl(v_{\Delta}=0 \text{ and } \tau_{\mathrm{ns}}>\tau_+^{\mathrm{stick}}\bigr), -\\[10pt] -\tau_-^{\mathrm{slip}}, -& -v_{\Delta}<0 -\;\text{or}\; -\bigl(v_{\Delta}=0 \text{ and } \tau_{\mathrm{ns}}<\tau_-^{\mathrm{stick}}\bigr). -\end{cases} -\label{eq:tau_c_complete} +\dot s. +\label{eq:s_dot_complete} \end{equation} -The belt-speed derivative is +The axial shift acceleration is obtained from \Eq{eq:s_ddot_final}: \begin{equation} -\dot{\hat v}_b = -\begin{cases} -\dot{\hat v}_{b,\mathrm{stick}}, -& -v_{\Delta}=0 -\;\text{and}\; -\tau_-^{\mathrm{stick}} \le \tau_{\mathrm{ns}} \le \tau_+^{\mathrm{stick}}, -\\[10pt] -\dot{\hat v}_{b,\mathrm{slip}}, -& -\text{otherwise.} -\end{cases} -\label{eq:vb_dot_complete} -\end{equation} - -\Eq{eq:tau_c_complete} and \Eq{eq:vb_dot_complete} define the ideal -piecewise analytical regime structure under -\AssRef{assump:ideal_friction} and \AssRef{assump:single_slip_interface}. -For numerical simulation, this branch logic is realized using the -regularized/blended transition law in -\Sec{sec:appendix-slip-regularization}, which smooths switching near -stick--slip transitions while preserving the same limiting branch behavior. - -% -------------------------------------------------------------- -\subsection{Governing ODEs} - -With the coupling torque and belt-speed derivative now closed, the rotational -dynamics are -\begin{align} -\dot\omega_p -&= -\frac{1}{I_p} -\left( -\tau_{\mathrm{eng}}(\omega_p) - \tau_c -\right), -\label{eq:omega_p_dot_complete} -\\[6pt] -\dot\omega_s -&= -\frac{1}{I_s} -\left( -R(s)\,\tau_c - \tau_{\mathrm{load}}(\omega_s) -\right), -\label{eq:omega_s_dot_complete} -\end{align} -from \Eq{eq:primary_rot_dyn} and \Eq{eq:secondary_rot_dyn}. - -The axial shift dynamics are -\begin{align} -\dot s &= \dot s, -\label{eq:s_dot_complete} -\\[6pt] \ddot s -&= += \frac{ -\Big(F_{p,\mathrm{ax}}(s,\omega_p) + F_{c,p}(s,\hat v_b)\Big) +\Big(F_{p,\mathrm{ax}}(s,\omega_p) + F_{c,p}(s,v_b)\Big) - -\Big(F_{s,\mathrm{ax}}(s,R(s)\tau_c) + F_{c,s}(s,\hat v_b)\Big) +\Big(F_{s,\mathrm{ax}}(s,\tau_s) + F_{c,s}(s,v_b)\Big) }{ m_{p,m} + m_{s,m} + \dfrac{\rho_b A_b L_b}{4} -}, +}. \label{eq:s_ddot_complete} -\end{align} -from \Eq{eq:s_ddot_final}. +\end{equation} -Finally, the belt-speed state evolves according to +Finally, the belt transport speed evolves according to the branch-selected belt +acceleration: \begin{equation} -\dot{\hat v}_b +\frac{\dd}{\dd t}v_b = -\text{the value selected by \Eq{eq:vb_dot_complete}}. -\label{eq:vb_dot_complete_repeat} +\dot v_b, +\qquad +\dot v_b +\text{ from } \Eq{eq:branch_closure_complete}. +\label{eq:vb_dot_complete} \end{equation} % -------------------------------------------------------------- @@ -7625,66 +7169,76 @@ \subsection{Model Evaluation Sequence} Given the current state \[ -\mathbf{x}(t)=\big(\omega_p,\omega_s,s,\dot s,\hat v_b\big), +\mathbf{x}(t)=\big(\omega_p,\omega_s,s,\dot s,v_b\big), \] the model is evaluated in the following order: \begin{enumerate} - \item Compute the pulley geometry from \(s\), including the effective - radii, centroid radii, wrap angles, and the ratio \(R(s)\). + \item Compute the pulley geometry from \(s\), including + \(r_p\), \(r_s\), \(r_{p,\mathrm{cm}}\), \(r_{s,\mathrm{cm}}\), + \(\phi_p\), and \(\phi_s\). - \item Compute \(\dot R(s,\dot s)\) from \Eq{eq:Rdot_of_states_complete}. + \item Compute the engine torque \(\tau_{\mathrm{eng}}(\omega_p)\) and the + reflected load torque \(\tau_{\mathrm{load}}(\omega_s)\). - \item Compute the pulley-imposed belt-line speeds - \(v_{p,\mathrm{cm}}\) and \(v_{s,\mathrm{cm}}\) from - \Eq{eq:belt_line_speeds_complete}, then form \(v_b^\ast\) from - \Eq{eq:vb_target_complete}. + \item Compute the no-slip candidate + \[ + \dot v_{b,\mathrm{ns}}, + \qquad + \tau_{p,\mathrm{ns}}, + \qquad + \tau_{s,\mathrm{ns}}, + \] + using \Eq{eq:no_slip_vb_dot}, \Eq{eq:tau_p_ns}, and \Eq{eq:tau_s_ns}. - \item Evaluate the no-slip torque demand \(\tau_{\mathrm{ns}}\) from - \Eq{eq:tau_ns_complete}. + \item Compute the primary and secondary no-slip admissibility quantities + from \Sec{sec:primary_tau_limits} and \Sec{sec:secondary_tau_limits}. - \item Evaluate the branchwise traction bounds - \(\tau_{c,p,\pm}^{\mathrm{stick}}\), - \(\tau_{c,p,\pm}^{\mathrm{slip}}\), - \(\tau_{c,s,\pm}^{\mathrm{stick}}\), - and \(\tau_{c,s,\pm}^{\mathrm{slip}}\), then combine them into - \(\tau_\pm^{\mathrm{stick}}\) and \(\tau_\pm^{\mathrm{slip}}\) using - \Eq{eq:tau_pm_stick_complete_explicit} and - \Eq{eq:tau_pm_slip_complete_explicit}. + \item Compute the relative contact speeds + \(v_{\mathrm{rel},p}\) and \(v_{\mathrm{rel},s}\) using + \Eq{eq:v_rel_complete}. - \item Evaluate the slip metric \(v_{\Delta}\) from - \Eq{eq:v_delta_complete}, then select both \(\tau_c\) and - \(\dot{\hat v}_b\) from \Eq{eq:tau_c_complete} and - \Eq{eq:vb_dot_complete}. + \item Select the active torque-transfer branch using + \Eq{eq:branch_closure_law}. This determines the applied values of + \[ + \dot v_b, + \qquad + \tau_p, + \qquad + \tau_s. + \] \item Evaluate the ODE right-hand sides \Eq{eq:omega_p_dot_complete}, \Eq{eq:omega_s_dot_complete}, \Eq{eq:s_dot_complete}, \Eq{eq:s_ddot_complete}, - and \Eq{eq:vb_dot_complete_repeat}. + and \Eq{eq:vb_dot_complete}. \end{enumerate} % -------------------------------------------------------------- \subsection{Complete Nonlinear ODE System} The complete closed CVT model is therefore + \begin{equation} \boxed{ \begin{aligned} \dot\omega_p &= -\frac{1}{I_p} -\left( -\tau_{\mathrm{eng}}(\omega_p) - \tau_c -\right), +\frac{ +\tau_{\mathrm{eng}}(\omega_p)-\tau_p +}{ +I_p +}, \\[6pt] \dot\omega_s &= -\frac{1}{I_s} -\left( -R(s)\,\tau_c - \tau_{\mathrm{load}}(\omega_s) -\right), +\frac{ +\tau_s-\tau_{\mathrm{load}}(\omega_s) +}{ +I_s +}, \\[6pt] \frac{\dd}{\dd t}s &= @@ -7693,56 +7247,37 @@ \subsection{Complete Nonlinear ODE System} \frac{\dd}{\dd t}\dot s &= \frac{ -\Big(F_{p,\mathrm{ax}}(s,\omega_p) + F_{c,p}(s,\hat v_b)\Big) +\Big(F_{p,\mathrm{ax}}(s,\omega_p) + F_{c,p}(s,v_b)\Big) - -\Big(F_{s,\mathrm{ax}}(s,R(s)\tau_c) + F_{c,s}(s,\hat v_b)\Big) +\Big(F_{s,\mathrm{ax}}(s,\tau_s) + F_{c,s}(s,v_b)\Big) }{ m_{p,m} + m_{s,m} + \dfrac{\rho_b A_b L_b}{4} }, \\[6pt] -\frac{\dd}{\dd t}\hat v_b +\frac{\dd}{\dd t}v_b &= -\begin{cases} -\dot{\hat v}_{b,\mathrm{stick}}, -& -v_{\Delta}=0 -\;\text{and}\; -\tau_-^{\mathrm{stick}} \le \tau_{\mathrm{ns}} \le \tau_+^{\mathrm{stick}}, -\\[8pt] -\dot{\hat v}_{b,\mathrm{slip}}, -& -\text{otherwise,} -\end{cases} +\dot v_b. \end{aligned} } \label{eq:final_cvt_system_complete} \end{equation} -with the algebraic closures -\[ -R=R(s), -\qquad -\dot R=\dot R(s,\dot s), -\qquad -v_b^\ast=\frac{1}{2}\bigl(r_{p,\mathrm{cm}}(s)\omega_p+r_{s,\mathrm{cm}}(s)\omega_s\bigr), -\] + +with the algebraic closure \[ -\dot{\hat v}_{b,\mathrm{stick}} -= -\dot r_{p,\mathrm{cm}}\,\omega_p -+ -r_{p,\mathrm{cm}}\,\dot\omega_p, -\qquad -\dot{\hat v}_{b,\mathrm{slip}} +(\dot v_b,\tau_p,\tau_s) = -\frac{v_b^\ast-\hat v_b}{T_b}, -\qquad -\tau_c \text{ from \Eq{eq:tau_c_complete}}. +\text{the active branch output from \Eq{eq:branch_closure_law}}. \] -In summary, the shift state determines the pulley geometry, the geometry -determines both the no-slip torque demand and the belt kinematics, the regime -law selects the transmitted coupling torque and belt-speed evolution, and those -quantities feed back into both the rotational and axial dynamics. +In summary, the state determines the pulley geometry and relative contact +speeds. The no-slip candidate determines the torque demand required for adhered +motion. The traction admissibility conditions determine whether that candidate +can be maintained. If not, the appropriate slip branch supplies saturated +contact torques and the resulting belt acceleration. These branch-selected +quantities then feed back into the rotational, belt, and axial shift dynamics, +closing the complete CVT state-space model. + + \section{Simulation Results} \label{sec:results} @@ -9152,12 +8687,12 @@ \subsection{Root Finding for Belt-Length Constraint} % -------------------------------------------------------------- \subsection{ODE Integration for Dynamic Simulation} -The full CVT state vector $\mathbf{x} = (\omega_p, \omega_s, s, \dot{s}, \hat v_b)$ +The full CVT state vector $\mathbf{x} = (\omega_p, \omega_s, s, \dot{s}, v_b)$ is integrated using \texttt{scipy.integrate.solve\_ivp}. The governing system is written as a first-order ODE system in \Eq{eq:final_cvt_system_complete}, with rotational sub-equations \Eq{eq:omega_p_dot_complete} and \Eq{eq:omega_s_dot_complete}, axial dynamics \Eq{eq:s_ddot_complete}, belt-speed -dynamics for $\hat v_b$, and algebraic geometry closures +dynamics for $v_b$, and algebraic geometry closures \Eq{eq:R_of_s_complete} and \Eq{eq:Rdot_of_states_complete}. Although \Eq{eq:s_ddot_complete} is second-order in $s$, introducing $(s,\dot s)$ as separate states converts the complete model to first-order form for time integration. The diff --git a/docs/CVT_Module_Formulation/illustrations/pulleyForces/primary_cvt_mechanisms.png b/docs/CVT_Module_Formulation/illustrations/pulleyForces/primary_cvt_mechanisms.png index 73e05fc9..f0befb8e 100644 Binary files a/docs/CVT_Module_Formulation/illustrations/pulleyForces/primary_cvt_mechanisms.png and b/docs/CVT_Module_Formulation/illustrations/pulleyForces/primary_cvt_mechanisms.png differ diff --git a/docs/CVT_Module_Formulation/illustrations/torque/cvt_overview_forces.png b/docs/CVT_Module_Formulation/illustrations/torque/cvt_overview_forces.png index bd6adf09..331968e8 100644 Binary files a/docs/CVT_Module_Formulation/illustrations/torque/cvt_overview_forces.png and b/docs/CVT_Module_Formulation/illustrations/torque/cvt_overview_forces.png differ diff --git a/docs/CVT_Module_Formulation/old_figures/pulleyForces/primary_cvt_mechanisms.png b/docs/CVT_Module_Formulation/old_figures/pulleyForces/primary_cvt_mechanisms.png new file mode 100644 index 00000000..73e05fc9 Binary files /dev/null and b/docs/CVT_Module_Formulation/old_figures/pulleyForces/primary_cvt_mechanisms.png differ diff --git a/docs/CVT_Module_Formulation/old_figures/torque/cvt_overview_forces.png b/docs/CVT_Module_Formulation/old_figures/torque/cvt_overview_forces.png new file mode 100644 index 00000000..bd6adf09 Binary files /dev/null and b/docs/CVT_Module_Formulation/old_figures/torque/cvt_overview_forces.png differ diff --git a/docs/Kai's folder of derivations/no stick belt EOM.png b/docs/Kai's folder of derivations/no stick belt EOM.png new file mode 100644 index 00000000..345a5813 Binary files /dev/null and b/docs/Kai's folder of derivations/no stick belt EOM.png differ diff --git a/docs/Kai's folder of derivations/second derivation for max torque.png b/docs/Kai's folder of derivations/second derivation for max torque.png new file mode 100644 index 00000000..340f0e3d Binary files /dev/null and b/docs/Kai's folder of derivations/second derivation for max torque.png differ diff --git a/frontend/src/components/scene3DViewer/Scene3DViewer.tsx b/frontend/src/components/scene3DViewer/Scene3DViewer.tsx index 910dfe3d..e3695ed9 100644 --- a/frontend/src/components/scene3DViewer/Scene3DViewer.tsx +++ b/frontend/src/components/scene3DViewer/Scene3DViewer.tsx @@ -134,7 +134,7 @@ export const Scene3DViewer = ({ replayController, className }: Scene3DViewerProp const secondaryRadius = firstDataPoint.drivetrain?.cvt_dynamics?.secondaryPulleyState?.radius ?? constants.max_sec_radius; const primaryWrapAngleDeg = firstDataPoint.drivetrain?.cvt_dynamics?.primaryPulleyState?.wrap_angle ?? 180; const secondaryWrapAngleDeg = firstDataPoint.drivetrain?.cvt_dynamics?.secondaryPulleyState?.wrap_angle ?? 180; - const shiftDistance = firstDataPoint.state?.shift_distance ?? 0; + const shiftDistance = firstDataPoint.state?.s ?? 0; const primaryWrapAngle = primaryWrapAngleDeg * (Math.PI / 180); const secondaryWrapAngle = secondaryWrapAngleDeg * (Math.PI / 180); @@ -180,11 +180,11 @@ export const Scene3DViewer = ({ replayController, className }: Scene3DViewerProp const primaryAngularPosition = degToRad(primaryAngularPositionDeg); const secondaryAngularPosition = degToRad(secondaryAngularPositionDeg); const secondaryHelixRotation = degToRad(secondaryHelixRotationDeg); - const shiftDistance = event.data.state?.shift_distance ?? 0; + const shiftDistance = event.data.state?.s ?? 0; // Get pulley states for belt calculation - const primaryRadius = event.data.drivetrain?.cvt_dynamics?.primaryPulleyState?.radius ?? constants.min_prim_radius; - const secondaryRadius = event.data.drivetrain?.cvt_dynamics?.secondaryPulleyState?.radius ?? constants.max_sec_radius; + const primaryRadius = event.data.contact_breakdown?.geometry?.primary_effective_radius ?? constants.min_prim_radius; + const secondaryRadius = event.data.contact_breakdown?.geometry?.secondary_effective_radius ?? constants.max_sec_radius; const primaryWrapAngleDeg = event.data.drivetrain?.cvt_dynamics?.primaryPulleyState?.wrap_angle ?? 180; const secondaryWrapAngleDeg = event.data.drivetrain?.cvt_dynamics?.secondaryPulleyState?.wrap_angle ?? 180; diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index f81a3652..3db5308d 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -32,7 +32,7 @@ export interface paths { * Get Constants * @description Get the physical constants and specifications used by the CVT simulator. * These values are useful for visualization and understanding the simulation parameters. - * Calculated values like max_shift and center_to_center are automatically computed. + * Calculated values like max_shift, center_to_center, and min/max effective CVT ratio are automatically computed. */ get: operations["get_constants_constants_get"]; put?: never; @@ -133,26 +133,56 @@ export interface components { primary_engagement: components["schemas"]["SolverResultModel"]; shift_initiation: components["schemas"]["SolverResultModel"]; }; - /** BeltStateBreakdownModel */ - BeltStateBreakdownModel: { - /** Is Stick */ - is_stick: boolean; - /** Relative Speed */ - relative_speed: number; - /** Primary Belt Speed */ - primary_belt_speed: number; - /** Secondary Belt Speed */ - secondary_belt_speed: number; - /** V B Star */ - v_b_star: number; - /** T B */ - T_b: number; - /** V B */ - v_b: number; - /** V B Compatible */ - v_b_compatible: number; - /** V B Dot */ - v_b_dot: number; + /** AnalysisStepDataModel */ + AnalysisStepDataModel: { + /** Time */ + time: number; + state: components["schemas"]["SystemStateModel"]; + derived_state: components["schemas"]["DerivedKinematicStateModel"]; + contact_breakdown: components["schemas"]["ContactDynamicsBreakdownModel"]; + }; + /** BeltWrapBreakdownModel */ + BeltWrapBreakdownModel: { + /** Wrap Angle */ + wrap_angle: number; + /** Axial Belt Force */ + axial_belt_force: number; + }; + /** BranchTorqueResultModel */ + BranchTorqueResultModel: { + /** Branch */ + branch: string; + /** Tau P */ + tau_p: number; + /** Tau S */ + tau_s: number; + }; + /** CVTGeometryResultModel */ + CVTGeometryResultModel: { + /** Effective Cvt Ratio */ + effective_cvt_ratio: number; + /** Effective Cvt Ratio Rate Of Change */ + effective_cvt_ratio_rate_of_change: number; + /** Primary Outer Radius */ + primary_outer_radius: number; + /** Primary Effective Radius */ + primary_effective_radius: number; + /** Primary Centroid Radius */ + primary_centroid_radius: number; + /** Primary Radius Rate Of Change */ + primary_radius_rate_of_change: number; + /** Secondary Outer Radius */ + secondary_outer_radius: number; + /** Secondary Effective Radius */ + secondary_effective_radius: number; + /** Secondary Centroid Radius */ + secondary_centroid_radius: number; + /** Secondary Radius Rate Of Change */ + secondary_radius_rate_of_change: number; + /** Primary Wrap Angle */ + primary_wrap_angle: number; + /** Secondary Wrap Angle */ + secondary_wrap_angle: number; }; /** * CarSpecs @@ -302,6 +332,16 @@ export interface components { * @description Center-to-center distance between pulleys in meters (calculated from belt and pulley geometry). */ readonly center_to_center: number; + /** + * Min Effective Cvt Ratio + * @description Minimum effective CVT ratio (unitless) at zero shift distance. + */ + readonly min_effective_cvt_ratio: number; + /** + * Max Effective Cvt Ratio + * @description Maximum effective CVT ratio (unitless) at max shift distance. + */ + readonly max_effective_cvt_ratio: number; }; /** CircularSegmentConfigModel */ CircularSegmentConfigModel: { @@ -319,16 +359,32 @@ export interface components { */ type: "circular"; }; + /** ContactDynamicsBreakdownModel */ + ContactDynamicsBreakdownModel: { + contact: components["schemas"]["ContactTorqueResultModel"]; + drivetrain: components["schemas"]["DrivetrainAccelerationBreakdownModel"]; + shift: components["schemas"]["CvtDynamicsBreakdownModel"]; + geometry: components["schemas"]["CVTGeometryResultModel"]; + }; + /** ContactTorqueResultModel */ + ContactTorqueResultModel: { + /** Tau P */ + tau_p: number; + /** Tau S */ + tau_s: number; + /** Branch */ + branch: string; + slip_metrics: components["schemas"]["SlipMetricsResultModel"]; + branch_result: components["schemas"]["BranchTorqueResultModel"]; + }; /** CvtDynamicsBreakdownModel */ CvtDynamicsBreakdownModel: { - primaryPulleyState: components["schemas"]["PulleyStateModel"]; - secondaryPulleyState: components["schemas"]["PulleyStateModel"]; + primaryPulleyState: components["schemas"]["PulleyForcesModel"]; + secondaryPulleyState: components["schemas"]["PulleyForcesModel"]; /** Friction */ friction: number; /** Acceleration */ acceleration: number; - /** Cvt Ratio */ - cvt_ratio: number; /** Net */ net: number; }; @@ -338,18 +394,36 @@ export interface components { car_velocity: number; /** Car Position */ car_position: number; + /** Belt Position */ + belt_position: number; /** Engine Angular Velocity */ engine_angular_velocity: number; /** Engine Angular Position */ engine_angular_position: number; }; - /** DrivetrainBreakdownModel */ - DrivetrainBreakdownModel: { - belt_slip: components["schemas"]["SlipBreakdownModel"]; - belt_state: components["schemas"]["BeltStateBreakdownModel"]; - primary_pulley: components["schemas"]["PrimaryPulleyDynamicsBreakdownModel"]; - secondary_pulley: components["schemas"]["SecondaryPulleyDynamicsBreakdownModel"]; - cvt_dynamics: components["schemas"]["CvtDynamicsBreakdownModel"]; + /** DrivetrainAccelerationBreakdownModel */ + DrivetrainAccelerationBreakdownModel: { + /** Ω P Dot */ + "\u03C9_p_dot": number; + /** Ω S Dot */ + "\u03C9_s_dot": number; + /** V B Dot */ + v_b_dot: number; + engine_breakdown: components["schemas"]["EngineTorqueBreakdownModel"]; + external_load_breakdown: components["schemas"]["ExternalLoadForceBreakdownModel"]; + /** Tau P */ + tau_p: number; + /** Tau S */ + tau_s: number; + }; + /** EngineTorqueBreakdownModel */ + EngineTorqueBreakdownModel: { + /** Engine Torque */ + engine_torque: number; + /** Engine Speed */ + engine_speed: number; + /** Engine Power */ + engine_power: number; }; /** ExternalLoadForceBreakdownModel */ ExternalLoadForceBreakdownModel: { @@ -370,12 +444,6 @@ export interface components { /** Net Torque At Secondary */ net_torque_at_secondary: number; }; - /** FormattedSimulationResultModel */ - FormattedSimulationResultModel: { - /** Data */ - data: components["schemas"]["TimeStepDataModel"][]; - termination: components["schemas"]["SimulationTerminationContextModel"]; - }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -407,6 +475,39 @@ export interface components { */ type: "linear"; }; + /** NoSlipBreakdownModel */ + NoSlipBreakdownModel: { + /** R P */ + r_p: number; + /** R S */ + r_s: number; + /** R P Dot */ + r_p_dot: number; + /** R S Dot */ + r_s_dot: number; + /** Tau Engine Over R P */ + tau_engine_over_r_p: number; + /** Tau Load Over R S */ + tau_load_over_r_s: number; + /** Primary Inertia Term */ + primary_inertia_term: number; + /** Secondary Inertia Term */ + secondary_inertia_term: number; + /** Numerator */ + numerator: number; + /** Denominator */ + denominator: number; + }; + /** NoSlipResultModel */ + NoSlipResultModel: { + /** V B Dot Ns */ + v_b_dot_ns: number; + /** Tau P Ns */ + tau_p_ns: number; + /** Tau S Ns */ + tau_s_ns: number; + breakdown: components["schemas"]["NoSlipBreakdownModel"]; + }; /** PiecewiseRampConfigModel */ PiecewiseRampConfigModel: { /** Segments */ @@ -419,71 +520,40 @@ export interface components { /** Net */ net: number; }; - /** PrimaryPulleyDynamicsBreakdownModel */ - PrimaryPulleyDynamicsBreakdownModel: { - /** Primary Pulley Drive Torque */ - primary_pulley_drive_torque: number; - /** Coupling Torque At Primary Pulley */ - coupling_torque_at_primary_pulley: number; - /** Power */ - power: number; - /** Primary Pulley Angular Velocity */ - primary_pulley_angular_velocity: number; - /** Primary Pulley Angular Acceleration */ - primary_pulley_angular_acceleration: number; - }; - /** PrimaryTorqueBoundsBreakdownModel */ - PrimaryTorqueBoundsBreakdownModel: { - /** Tau Lower */ - tau_lower: number; - /** Tau Upper */ - tau_upper: number; - numerator: components["schemas"]["PrimaryTorqueNumeratorBreakdownModel"]; - denominator_upper: components["schemas"]["PrimaryTorqueDenominatorBreakdownModel"]; - denominator_lower: components["schemas"]["PrimaryTorqueDenominatorBreakdownModel"]; - }; - /** PrimaryTorqueDenominatorBreakdownModel */ - PrimaryTorqueDenominatorBreakdownModel: { - /** Inverse Radius Term */ - inverse_radius_term: number; - /** Inertial Feedback Term */ - inertial_feedback_term: number; - /** Net */ - net: number; - }; - /** PrimaryTorqueNumeratorBreakdownModel */ - PrimaryTorqueNumeratorBreakdownModel: { - /** Clamping Term */ - clamping_term: number; - /** Load Term */ - load_term: number; - /** Shift Term */ - shift_term: number; - /** Net */ - net: number; + /** PrimaryTorqueAdmissibilityBreakdownModel */ + PrimaryTorqueAdmissibilityBreakdownModel: { + /** Shift Distance */ + shift_distance: number; + /** Wrap Angle */ + wrap_angle: number; + /** Effective Radius */ + effective_radius: number; + /** Centroid Radius */ + centroid_radius: number; + /** Centroid Radius Rate */ + centroid_radius_rate: number; + /** Axial Clamping Force */ + axial_clamping_force: number; + /** Belt Centripetal Term */ + belt_centripetal_term: number; + /** Friction Coefficient */ + friction_coefficient: number; + /** Sheave Half Angle */ + sheave_half_angle: number; + /** Tau P Stick Limit */ + tau_p_stick_limit: number; + /** Tau P Stick Upper */ + tau_p_stick_upper: number; + /** Tau P Stick Lower */ + tau_p_stick_lower: number; }; /** PulleyForcesModel */ PulleyForcesModel: { - /** Axial Clamping Force */ - axial_clamping_force: number; - /** Axial Centrifugal From Belt */ - axial_centrifugal_from_belt: number; - /** Axial Force Total */ - axial_force_total: number; - }; - /** PulleyStateModel */ - PulleyStateModel: { - forces: components["schemas"]["PulleyForcesModel"]; - /** Wrap Angle */ - wrap_angle: number; - /** Radius */ - radius: number; - /** Angular Velocity */ - angular_velocity: number; - /** Angular Position */ - angular_position: number; - /** Breakdown */ - breakdown: components["schemas"]["PrimaryForceBreakdownModel"] | components["schemas"]["SecondaryForceBreakdownModel"]; + /** Pulley Breakdown */ + pulley_breakdown: components["schemas"]["PrimaryForceBreakdownModel"] | components["schemas"]["SecondaryForceBreakdownModel"]; + belt_wrap: components["schemas"]["BeltWrapBreakdownModel"]; + /** Net */ + net: number; }; /** RampPreviewResponse */ RampPreviewResponse: { @@ -505,47 +575,46 @@ export interface components { /** Net */ net: number; }; - /** SecondaryPulleyDynamicsBreakdownModel */ - SecondaryPulleyDynamicsBreakdownModel: { - /** Coupling Torque At Secondary Pulley */ - coupling_torque_at_secondary_pulley: number; - /** External Load Torque At Secondary Pulley */ - external_load_torque_at_secondary_pulley: number; - external_forces: components["schemas"]["ExternalLoadForceBreakdownModel"]; - /** Secondary Pulley Angular Acceleration */ - secondary_pulley_angular_acceleration: number; - }; - /** SecondaryTorqueBoundsBreakdownModel */ - SecondaryTorqueBoundsBreakdownModel: { - /** Tau Negative */ - tau_negative: number; - /** Tau Positive */ - tau_positive: number; - numerator: components["schemas"]["SecondaryTorqueNumeratorBreakdownModel"]; - denominator_positive: components["schemas"]["SecondaryTorqueDenominatorBreakdownModel"]; - denominator_negative: components["schemas"]["SecondaryTorqueDenominatorBreakdownModel"]; - }; - /** SecondaryTorqueDenominatorBreakdownModel */ - SecondaryTorqueDenominatorBreakdownModel: { - /** Inverse Radius Term */ - inverse_radius_term: number; - /** Helix Feedback Term */ - helix_feedback_term: number; - /** Inertial Feedback Term */ - inertial_feedback_term: number; - /** Net */ - net: number; - }; - /** SecondaryTorqueNumeratorBreakdownModel */ - SecondaryTorqueNumeratorBreakdownModel: { - /** Spring Term */ - spring_term: number; - /** Load Term */ - load_term: number; - /** Shift Term */ - shift_term: number; - /** Net */ - net: number; + /** SecondaryTorqueAdmissibilityBreakdownModel */ + SecondaryTorqueAdmissibilityBreakdownModel: { + /** Shift Distance */ + shift_distance: number; + /** Wrap Angle */ + wrap_angle: number; + /** Effective Radius */ + effective_radius: number; + /** Centroid Radius */ + centroid_radius: number; + /** Centroid Radius Rate */ + centroid_radius_rate: number; + /** Helix Rotation */ + helix_rotation: number; + /** Helix Rotation Rate */ + helix_rotation_rate: number; + /** Spring Torsion Term */ + spring_torsion_term: number; + /** Spring Comp Term */ + spring_comp_term: number; + /** Belt Centripetal Term */ + belt_centripetal_term: number; + /** Friction Coefficient */ + friction_coefficient: number; + /** Sheave Half Angle */ + sheave_half_angle: number; + /** Denominator Upper */ + denominator_upper: number; + /** Denominator Lower */ + denominator_lower: number; + /** Tau Stick Upper */ + tau_stick_upper: number; + /** Tau Stick Lower */ + tau_stick_lower: number; + }; + /** SimulationAnalysisResultModel */ + SimulationAnalysisResultModel: { + /** Data */ + data: components["schemas"]["AnalysisStepDataModel"][]; + termination: components["schemas"]["SimulationTerminationContextModel"]; }; /** SimulationArgsInput */ SimulationArgsInput: { @@ -601,22 +670,22 @@ export interface components { [key: string]: number | string | boolean; }; }; - /** SlipBreakdownModel */ - SlipBreakdownModel: { - /** Coupling Torque */ - coupling_torque: number; - /** Torque Demand */ - torque_demand: number; - /** Tau Upper */ - tau_upper: number; - /** Tau Lower */ - tau_lower: number; - primary_tau_bounds: components["schemas"]["PrimaryTorqueBoundsBreakdownModel"]; - secondary_tau_bounds: components["schemas"]["SecondaryTorqueBoundsBreakdownModel"]; - /** Effective Cvt Ratio Time Derivative */ - effective_cvt_ratio_time_derivative: number; - /** Is Slipping */ - is_slipping: boolean; + /** SlipMetricsResultModel */ + SlipMetricsResultModel: { + /** Primary Relative Speed */ + primary_relative_speed: number; + /** Secondary Relative Speed */ + secondary_relative_speed: number; + /** Primary Slip Direction */ + primary_slip_direction: number; + /** Secondary Slip Direction */ + secondary_slip_direction: number; + /** Primary Admissible */ + primary_admissible: boolean; + /** Secondary Admissible */ + secondary_admissible: boolean; + admissibility: components["schemas"]["TorqueAdmissibilityResultModel"]; + no_slip: components["schemas"]["NoSlipResultModel"]; }; /** SolverResultModel */ SolverResultModel: { @@ -643,7 +712,7 @@ export interface components { * @constant */ type: "complete"; - data: components["schemas"]["FormattedSimulationResultModel"]; + data: components["schemas"]["SimulationAnalysisResultModel"]; }; /** StreamErrorMessage */ StreamErrorMessage: { @@ -669,24 +738,29 @@ export interface components { }; /** SystemStateModel */ SystemStateModel: { - /** Shift Distance */ - shift_distance: number; - /** Shift Velocity */ - shift_velocity: number; - /** Primary Pulley Angular Velocity */ - primary_pulley_angular_velocity: number; - /** Secondary Pulley Angular Velocity */ - secondary_pulley_angular_velocity: number; + /** S */ + s: number; + /** S Dot */ + s_dot: number; + /** Ω P */ + "\u03C9_p": number; + /** Ω S */ + "\u03C9_s": number; /** V B */ v_b: number; }; - /** TimeStepDataModel */ - TimeStepDataModel: { - /** Time */ - time: number; - state: components["schemas"]["SystemStateModel"]; - derived_state: components["schemas"]["DerivedKinematicStateModel"]; - drivetrain: components["schemas"]["DrivetrainBreakdownModel"]; + /** TorqueAdmissibilityResultModel */ + TorqueAdmissibilityResultModel: { + primary: components["schemas"]["PrimaryTorqueAdmissibilityBreakdownModel"]; + secondary: components["schemas"]["SecondaryTorqueAdmissibilityBreakdownModel"]; + /** Primary Tau P Stick Upper */ + primary_tau_p_stick_upper: number; + /** Primary Tau P Stick Lower */ + primary_tau_p_stick_lower: number; + /** Secondary Tau Stick Upper */ + secondary_tau_stick_upper: number; + /** Secondary Tau Stick Lower */ + secondary_tau_stick_lower: number; }; /** ValidationError */ ValidationError: { @@ -787,7 +861,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["FormattedSimulationResultModel"]; + "application/json": components["schemas"]["SimulationAnalysisResultModel"]; }; }; /** @description Validation Error */ diff --git a/frontend/src/types/graph.ts b/frontend/src/types/graph.ts index 5fa3b5e6..56637aaf 100644 --- a/frontend/src/types/graph.ts +++ b/frontend/src/types/graph.ts @@ -3,7 +3,7 @@ import type { RunResponse } from "@utils/api"; import type { BaseUnitType } from "@utils/conversion"; import { UNIT_PRESETS, getTargetUnit } from "@utils/conversion"; -type DataPoint = RunResponse['data'][number]; // TODO: Move to somewhere else (maybe replay controller file) +type DataPoint = RunResponse['data'][number]; type AccessorStrategy = (point: DataPoint) => number; @@ -18,144 +18,152 @@ export type GraphCategory = { graphs: GraphConfig[]; }; +// Basic accessors export const timeAccessor: AccessorStrategy = (point) => point.time; const positionAccessor: AccessorStrategy = (point) => point.derived_state.car_position; const velocityAccessor: AccessorStrategy = (point) => point.derived_state.car_velocity; -const accelerationAccessor: AccessorStrategy = (point) => point.drivetrain.secondary_pulley.secondary_pulley_angular_acceleration; +const accelerationAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.ω_s_dot; -// Temp -const couplingTorqueAtWheels: AccessorStrategy = (point) => point.drivetrain.secondary_pulley.coupling_torque_at_secondary_pulley; -const loadTorqueAtWheels: AccessorStrategy = (point) => point.drivetrain.secondary_pulley.external_load_torque_at_secondary_pulley; - -const couplingTorqueAtEngine: AccessorStrategy = (point) => point.drivetrain.primary_pulley.coupling_torque_at_primary_pulley; +// Torques / coupling +const couplingTorqueAtWheels: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.tau_s; +const loadTorqueAtWheels: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.net_torque_at_secondary; +const couplingTorqueAtEngine: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.tau_p; // Engine and CVT stuff -const cvtRatioAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.cvt_ratio; -const engineRpmAccessor: AccessorStrategy = (point) => point.drivetrain.primary_pulley.primary_pulley_angular_velocity; -const engineTorqueAccessor: AccessorStrategy = (point) => point.drivetrain.primary_pulley.primary_pulley_drive_torque; -const cvtRatioRateOfChangeAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.effective_cvt_ratio_time_derivative; -const enginePowerAccessor: AccessorStrategy = (point) => point.drivetrain.primary_pulley.power; -const cvtAccelerationAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.acceleration; +const cvtRatioAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.effective_cvt_ratio; +const cvtRatioRateOfChangeAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.effective_cvt_ratio_rate_of_change; +const primaryOuterRadiusAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.primary_outer_radius; +const primaryOuterRadiusRateAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.primary_radius_rate_of_change; +const primaryEffectiveRadiusAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.primary_effective_radius; +const secondaryOuterRadiusAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.secondary_outer_radius; +const secondaryOuterRadiusRateAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.secondary_radius_rate_of_change; +const secondaryEffectiveRadiusAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.secondary_effective_radius; +const primaryCentroidRadiusAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.primary_centroid_radius; +const primaryCentroidRadiusRateAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.primary_radius_rate_of_change; +const secondaryCentroidRadiusAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.secondary_centroid_radius; +const secondaryCentroidRadiusRateAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.secondary_radius_rate_of_change; +const primaryWrapAngleAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.primary_wrap_angle; +const secondaryWrapAngleAccessor: AccessorStrategy = (point) => point.contact_breakdown.geometry.secondary_wrap_angle; +const engineRpmAccessor: AccessorStrategy = (point) => point.derived_state.engine_angular_velocity; +const engineTorqueAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.engine_breakdown.engine_torque; +const enginePowerAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.engine_breakdown.engine_power; +const cvtAccelerationAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.acceleration; // Slip model accessors -const coupling_torqueAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.coupling_torque; -const torque_demandAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.torque_demand; -const tau_upperAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.tau_upper; -const tau_lowerAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.tau_lower; -const primary_tau_upperAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.primary_tau_bounds.tau_upper; -const primary_tau_lowerAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.primary_tau_bounds.tau_lower; -const secondary_tau_positiveAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.secondary_tau_bounds.tau_positive; -const secondary_tau_negativeAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.secondary_tau_bounds.tau_negative; -const relativeVelocityAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.relative_speed; -const isSlippingAccessor: AccessorStrategy = (point) => point.drivetrain.belt_slip.is_slipping ? 1 : 0; - -// Belt model accessors -const primaryBeltSpeedAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.primary_belt_speed; -const secondaryBeltSpeedAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.secondary_belt_speed; -const beltSpeedAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.v_b; -const beltCompatibleSpeedAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.v_b_compatible; -const beltTargetSpeedAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.v_b_star; -const beltSpeedDerivativeAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.v_b_dot; -const beltRelaxationTimeAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.T_b; -const beltIsStickAccessor: AccessorStrategy = (point) => point.drivetrain.belt_state.is_stick ? 1 : 0; +const coupling_torqueAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.tau_p; +const torque_demandAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.no_slip.tau_p_ns; +const secondaryTorqueDemandAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.no_slip.tau_s_ns; +const tau_upperAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.admissibility.primary_tau_p_stick_upper; +const tau_lowerAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.admissibility.primary_tau_p_stick_lower; +const primary_tau_upperAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.admissibility.primary_tau_p_stick_upper; +const primary_tau_lowerAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.admissibility.primary_tau_p_stick_lower; +const secondary_tau_positiveAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.admissibility.secondary_tau_stick_upper; +const secondary_tau_negativeAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.admissibility.secondary_tau_stick_lower; +const branchNoSlipAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.branch === 'NO_SLIP' ? 1 : 0; +const branchPrimarySlipAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.branch === 'PRIMARY_SLIP' ? 1 : 0; +const branchSecondarySlipAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.branch === 'SECONDARY_SLIP' ? 1 : 0; +const branchBothSlipAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.branch === 'BOTH_SLIP' ? 1 : 0; +const primaryRelativeVelocityAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.primary_relative_speed; +const secondaryRelativeVelocityAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.slip_metrics.secondary_relative_speed; +const beltSpeedAccessor: AccessorStrategy = (point) => point.state.v_b; +const appliedPrimaryTorqueAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.tau_p; +const appliedSecondaryTorqueAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.tau_s; +const primaryAngularAccelerationAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.ω_p_dot; +const secondaryAngularAccelerationAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.ω_s_dot; +const isSlippingAccessor: AccessorStrategy = (point) => point.contact_breakdown.contact.branch === 'NO_SLIP' ? 0 : 1; // External load -const rollingResistanceForceAccessor: AccessorStrategy = (point) => point.drivetrain.secondary_pulley.external_forces.rolling_resistance_force; -const inclineForceAccessor: AccessorStrategy = (point) => point.drivetrain.secondary_pulley.external_forces.incline_force; -const dragForceAccessor: AccessorStrategy = (point) => point.drivetrain.secondary_pulley.external_forces.drag_force; -const totalExternalLoadForceAtCarAccessor: AccessorStrategy = (point) => - (point.drivetrain.secondary_pulley.external_forces as DataPoint['drivetrain']['secondary_pulley']['external_forces'] & { net_force_at_car: number }).net_force_at_car; -const rollingResistanceTorqueAtSecondaryAccessor: AccessorStrategy = (point) => - (point.drivetrain.secondary_pulley.external_forces as DataPoint['drivetrain']['secondary_pulley']['external_forces'] & { rolling_resistance_torque_at_secondary: number }).rolling_resistance_torque_at_secondary; -const inclineTorqueAtSecondaryAccessor: AccessorStrategy = (point) => - (point.drivetrain.secondary_pulley.external_forces as DataPoint['drivetrain']['secondary_pulley']['external_forces'] & { incline_torque_at_secondary: number }).incline_torque_at_secondary; -const dragTorqueAtSecondaryAccessor: AccessorStrategy = (point) => - (point.drivetrain.secondary_pulley.external_forces as DataPoint['drivetrain']['secondary_pulley']['external_forces'] & { drag_torque_at_secondary: number }).drag_torque_at_secondary; -const totalExternalLoadTorqueAtSecondaryAccessor: AccessorStrategy = (point) => - (point.drivetrain.secondary_pulley.external_forces as DataPoint['drivetrain']['secondary_pulley']['external_forces'] & { net_torque_at_secondary: number }).net_torque_at_secondary; +const rollingResistanceForceAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.rolling_resistance_force; +const inclineForceAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.incline_force; +const dragForceAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.drag_force; +const totalExternalLoadForceAtCarAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.net_force_at_car; +const rollingResistanceTorqueAtSecondaryAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.rolling_resistance_torque_at_secondary; +const inclineTorqueAtSecondaryAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.incline_torque_at_secondary; +const dragTorqueAtSecondaryAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.drag_torque_at_secondary; +const totalExternalLoadTorqueAtSecondaryAccessor: AccessorStrategy = (point) => point.contact_breakdown.drivetrain.external_load_breakdown.net_torque_at_secondary; -// Overall pulley clamping force (Axial) -const primaryAxialClampingForceAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.primaryPulleyState.forces.axial_clamping_force; -const secondaryAxialClampingForceAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.secondaryPulleyState.forces.axial_clamping_force; -const primaryAxialCentrifugalFromBeltAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.primaryPulleyState.forces.axial_centrifugal_from_belt; -const secondaryAxialCentrifugalFromBeltAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.secondaryPulleyState.forces.axial_centrifugal_from_belt; -const primaryAxialForceTotalAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.primaryPulleyState.forces.axial_force_total; -const secondaryAxialForceTotalAccessor: AccessorStrategy = (point) => point.drivetrain.cvt_dynamics.secondaryPulleyState.forces.axial_force_total; +// Pulley / axial forces +const primaryAxialClampingForceAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.primaryPulleyState.pulley_breakdown.net; +const secondaryAxialClampingForceAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.secondaryPulleyState.pulley_breakdown.net; +const primaryAxialCentrifugalFromBeltAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.primaryPulleyState.belt_wrap.axial_belt_force; +const secondaryAxialCentrifugalFromBeltAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.secondaryPulleyState.belt_wrap.axial_belt_force; +const primaryAxialForceTotalAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.primaryPulleyState.net; +const secondaryAxialForceTotalAccessor: AccessorStrategy = (point) => point.contact_breakdown.shift.secondaryPulleyState.net; -// Helper function to extract values from breakdown with proper error handling +// Pulley breakdown helpers function getBreakdownValue( - breakdown: DataPoint['drivetrain']['cvt_dynamics']['primaryPulleyState']['breakdown'] | - DataPoint['drivetrain']['cvt_dynamics']['secondaryPulleyState']['breakdown'], - propertyPath: string[], + breakdown: DataPoint['contact_breakdown']['shift']['primaryPulleyState']['pulley_breakdown'] | + DataPoint['contact_breakdown']['shift']['secondaryPulleyState']['pulley_breakdown'], + propertyPath: string[], contextName: string = "breakdown" ): T { let current: unknown = breakdown; - for (const prop of propertyPath) { - if (typeof current !== 'object' || current === null || !(prop in current)) { - throw new Error(`Missing property '${prop}' in ${contextName} breakdown. Expected path: ${propertyPath.join('.')}. This indicates a data structure mismatch.`); + if (typeof current !== 'object' || current === null || !(prop in (current as Record))) { + throw new Error(`Missing property '${prop}' in ${contextName} breakdown. Expected path: ${propertyPath.join('.')}.`); } - current = (current as Record)[prop]; - - if (current == null) { - throw new Error(`Property '${prop}' is null/undefined in ${contextName} breakdown. Expected path: ${propertyPath.join('.')}. This indicates a data structure mismatch.`); - } + if (current == null) throw new Error(`Property '${prop}' is null/undefined in ${contextName} breakdown.`); } - return current as T; } const primaryFlyweightForceAccessor: AccessorStrategy = (point) => { - const prf = point.drivetrain.cvt_dynamics.primaryPulleyState.breakdown; + const prf = point.contact_breakdown.shift.primaryPulleyState.pulley_breakdown; return getBreakdownValue(prf, ['flyweightForce', 'net'], "primary pulley"); }; - const rawFlyweightCentrifugalForce: AccessorStrategy = (point) => { - const prf = point.drivetrain.cvt_dynamics.primaryPulleyState.breakdown; + const prf = point.contact_breakdown.shift.primaryPulleyState.pulley_breakdown; return getBreakdownValue(prf, ['flyweightForce', 'centrifugal_force'], "primary pulley"); }; - const primarySpringForceAccessor: AccessorStrategy = (point) => { - const prf = point.drivetrain.cvt_dynamics.primaryPulleyState.breakdown; + const prf = point.contact_breakdown.shift.primaryPulleyState.pulley_breakdown; return getBreakdownValue(prf, ['springForce', 'net'], "primary pulley"); }; - const primaryRampAngleAccessor: AccessorStrategy = (point) => { - const prf = point.drivetrain.cvt_dynamics.primaryPulleyState.breakdown; + const prf = point.contact_breakdown.shift.primaryPulleyState.pulley_breakdown; return getBreakdownValue(prf, ['flyweightForce', 'angle'], "primary pulley"); }; const secondaryHelixFeedbackTorqueAccessor: AccessorStrategy = (point) => { - const srf = point.drivetrain.cvt_dynamics.secondaryPulleyState.breakdown; + const srf = point.contact_breakdown.shift.secondaryPulleyState.pulley_breakdown; return getBreakdownValue(srf, ['helix_force', 'feedbackTorque'], "secondary pulley"); }; - const secondaryHelixSpringTorqueAccessor: AccessorStrategy = (point) => { - const srf = point.drivetrain.cvt_dynamics.secondaryPulleyState.breakdown; + const srf = point.contact_breakdown.shift.secondaryPulleyState.pulley_breakdown; return getBreakdownValue(srf, ['helix_force', 'springTorque', 'net'], "secondary pulley"); -}; - +}; const secondaryHelixForceAccessor: AccessorStrategy = (point) => { - const srf = point.drivetrain.cvt_dynamics.secondaryPulleyState.breakdown; + const srf = point.contact_breakdown.shift.secondaryPulleyState.pulley_breakdown; return getBreakdownValue(srf, ['helix_force', 'net'], "secondary pulley"); }; - const secondarySpringCompForceAccessor: AccessorStrategy = (point) => { - const srf = point.drivetrain.cvt_dynamics.secondaryPulleyState.breakdown; + const srf = point.contact_breakdown.shift.secondaryPulleyState.pulley_breakdown; return getBreakdownValue(srf, ['springCompForce', 'net'], "secondary pulley"); -}; +}; -// Mapping from accessor to unit type +// Mapping from accessor to unit type (belt-state entries intentionally omitted) export const accessorToUnit = new Map([ [timeAccessor, 'time'], [positionAccessor, 'distance'], [velocityAccessor, 'velocity'], [accelerationAccessor, 'angular_acceleration'], [cvtRatioAccessor, 'dimensionless'], + [cvtRatioRateOfChangeAccessor, 'dimensionless_rate'], + [primaryOuterRadiusAccessor, 'distance'], + [primaryOuterRadiusRateAccessor, 'velocity'], + [primaryEffectiveRadiusAccessor, 'distance'], + [secondaryOuterRadiusAccessor, 'distance'], + [secondaryOuterRadiusRateAccessor, 'velocity'], + [secondaryEffectiveRadiusAccessor, 'distance'], + [primaryCentroidRadiusAccessor, 'distance'], + [primaryCentroidRadiusRateAccessor, 'velocity'], + [secondaryCentroidRadiusAccessor, 'distance'], + [secondaryCentroidRadiusRateAccessor, 'velocity'], + [primaryWrapAngleAccessor, 'angle'], + [secondaryWrapAngleAccessor, 'angle'], [engineRpmAccessor, 'angular_velocity'], [engineTorqueAccessor, 'torque'], - [cvtRatioRateOfChangeAccessor, 'dimensionless_rate'], [enginePowerAccessor, 'power'], [coupling_torqueAccessor, 'torque'], [tau_upperAccessor, 'torque'], @@ -164,16 +172,19 @@ export const accessorToUnit = new Map([ [primary_tau_lowerAccessor, 'torque'], [secondary_tau_positiveAccessor, 'torque'], [secondary_tau_negativeAccessor, 'torque'], - [relativeVelocityAccessor, 'velocity'], - [primaryBeltSpeedAccessor, 'velocity'], - [secondaryBeltSpeedAccessor, 'velocity'], + [branchNoSlipAccessor, 'dimensionless'], + [branchPrimarySlipAccessor, 'dimensionless'], + [branchSecondarySlipAccessor, 'dimensionless'], + [branchBothSlipAccessor, 'dimensionless'], + [primaryRelativeVelocityAccessor, 'velocity'], + [secondaryRelativeVelocityAccessor, 'velocity'], [beltSpeedAccessor, 'velocity'], - [beltCompatibleSpeedAccessor, 'velocity'], - [beltTargetSpeedAccessor, 'velocity'], - [beltSpeedDerivativeAccessor, 'acceleration'], - [beltRelaxationTimeAccessor, 'time'], - [beltIsStickAccessor, 'dimensionless'], + [appliedPrimaryTorqueAccessor, 'torque'], + [appliedSecondaryTorqueAccessor, 'torque'], + [primaryAngularAccelerationAccessor, 'angular_acceleration'], + [secondaryAngularAccelerationAccessor, 'angular_acceleration'], [torque_demandAccessor, 'torque'], + [secondaryTorqueDemandAccessor, 'torque'], [primaryFlyweightForceAccessor, 'force'], [rawFlyweightCentrifugalForce, 'force'], [primarySpringForceAccessor, 'force'], @@ -201,457 +212,466 @@ export const accessorToUnit = new Map([ [couplingTorqueAtEngine, 'torque'], ]); -// Helper function to get unit label for an accessor +// Helper to get axis unit label function getAxisUnit(accessor: AccessorStrategy): string { const unitType = accessorToUnit.get(accessor); - if (!unitType) return 'No unit associated with accessor!'; - - // Get BAJA unit as default + if (!unitType) return ''; const unit = getTargetUnit(unitType, UNIT_PRESETS.BAJA); return unit || ''; } +// Graph categories (belt debug removed) export const graphCategories: GraphCategory[] = [ { title: "Kinematics", graphs: [ - { - xAccessor: timeAccessor, - yAccessor: [positionAccessor], - config: { - title: "Position vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Position", type: "value", unit: getAxisUnit(positionAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopLeft, - } - }, - { - xAccessor: timeAccessor, - yAccessor: [velocityAccessor], - config: { - title: "Velocity vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Velocity", type: "value", unit: getAxisUnit(velocityAccessor) }, - showXLine: true, - showYLine: true, - tooltipPosition: TooltipPosition.BottomRight, - } - }, - { - xAccessor: timeAccessor, - yAccessor: [accelerationAccessor], - config: { - title: "Acceleration vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Acceleration", type: "value", unit: getAxisUnit(accelerationAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - }, -]}, -{ - title: "Acceleration of Engine and Car", - graphs: [ - // Graphs for looking at accelration of engine and wheels as separate systems - { - xAccessor: timeAccessor, - yAccessor: [couplingTorqueAtWheels, loadTorqueAtWheels], - config: { - title: "Torques at Wheels vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Torque", type: "value", unit: getAxisUnit(couplingTorqueAtWheels) }, - seriesNames: ["Coupling Torque at Secondary", "Load Torque at Secondary"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, + { + xAccessor: timeAccessor, + yAccessor: [positionAccessor], + config: { + title: "Position vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Position", type: "value", unit: getAxisUnit(positionAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopLeft, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [velocityAccessor], + config: { + title: "Velocity vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Velocity", type: "value", unit: getAxisUnit(velocityAccessor) }, + showXLine: true, + showYLine: true, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [accelerationAccessor], + config: { + title: "Acceleration vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Acceleration", type: "value", unit: getAxisUnit(accelerationAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } } - }, - { - xAccessor: timeAccessor, - yAccessor: [couplingTorqueAtEngine, engineTorqueAccessor], - config: { - title: "Torques at Engine vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Torque", type: "value", unit: getAxisUnit(couplingTorqueAtEngine) }, - seriesNames: ["Coupling Torque at Engine", "Engine Torque"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - } - ] -}, -{ - title: "External Load", - graphs: [ - /** EXTERNAL LOAD */ - { - xAccessor: velocityAccessor, - yAccessor: [totalExternalLoadForceAtCarAccessor, rollingResistanceForceAccessor, inclineForceAccessor, dragForceAccessor], - config: { - title: "External Load Forces at Car vs Vehicle Speed", - xAxis: { name: "Vehicle Speed", type: "value", unit: getAxisUnit(velocityAccessor) }, - yAxis: { name: "Force", type: "value", unit: getAxisUnit(inclineForceAccessor) }, - seriesNames: ["Total (Car)", "Rolling Resistance", "Incline Force", "Air Resistance"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopLeft, - } - }, - { - xAccessor: velocityAccessor, - yAccessor: [totalExternalLoadTorqueAtSecondaryAccessor, rollingResistanceTorqueAtSecondaryAccessor, inclineTorqueAtSecondaryAccessor, dragTorqueAtSecondaryAccessor], - config: { - title: "External Load Torques at Secondary vs Vehicle Speed", - xAxis: { name: "Vehicle Speed", type: "value", unit: getAxisUnit(velocityAccessor) }, - yAxis: { name: "Torque", type: "value", unit: getAxisUnit(totalExternalLoadTorqueAtSecondaryAccessor) }, - seriesNames: ["Total (Secondary)", "Rolling Resistance", "Incline Torque", "Air Resistance Torque"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopLeft, - } - }, -]}, -{ - title: "CVT Ratio", - graphs: [ - /** CVT RATIO GRAPHS */ - { - xAccessor: timeAccessor, - yAccessor: [cvtRatioAccessor], - config: { - title: "CVT Ratio vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "CVT Ratio", type: "value", unit: getAxisUnit(cvtRatioAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - }, - { - xAccessor: timeAccessor, - yAccessor: [cvtRatioRateOfChangeAccessor], - config: { - title: "CVT Ratio Rate of Change vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "CVT Ratio Rate of Change", type: "value", unit: getAxisUnit(cvtRatioRateOfChangeAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } - }, - { - xAccessor: velocityAccessor, - yAccessor: [engineRpmAccessor], - config: { - title: "Shift Curve (Engine RPM vs Vehicle Speed)", - xAxis: { name: "Vehicle Speed", type: "value", unit: getAxisUnit(velocityAccessor) }, - yAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } + ] }, -]}, -{ - title: "Engine", - graphs: [ - /** ENGINE GRAPHS */ { - xAccessor: timeAccessor, - yAccessor: [engineRpmAccessor], - config: { - title: "Engine RPM vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } - }, - { - xAccessor: timeAccessor, - yAccessor: [engineTorqueAccessor], - config: { - title: "Engine Torque vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Engine Torque", type: "value", unit: getAxisUnit(engineTorqueAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } + title: "Acceleration of Engine and Car", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [couplingTorqueAtWheels, loadTorqueAtWheels], + config: { + title: "Torques at Wheels vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Torque", type: "value", unit: getAxisUnit(couplingTorqueAtWheels) }, + seriesNames: ["Coupling Torque at Secondary", "Load Torque at Secondary"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [couplingTorqueAtEngine, engineTorqueAccessor], + config: { + title: "Torques at Engine vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Torque", type: "value", unit: getAxisUnit(couplingTorqueAtEngine) }, + seriesNames: ["Coupling Torque at Engine", "Engine Torque"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + } + ] }, { - xAccessor: timeAccessor, - yAccessor: [enginePowerAccessor], - config: { - title: "Engine Power vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Engine Power", type: "value", unit: getAxisUnit(enginePowerAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } + title: "External Load", + graphs: [ + { + xAccessor: velocityAccessor, + yAccessor: [totalExternalLoadForceAtCarAccessor, rollingResistanceForceAccessor, inclineForceAccessor, dragForceAccessor], + config: { + title: "External Load Forces at Car vs Vehicle Speed", + xAxis: { name: "Vehicle Speed", type: "value", unit: getAxisUnit(velocityAccessor) }, + yAxis: { name: "Force", type: "value", unit: getAxisUnit(inclineForceAccessor) }, + seriesNames: ["Total (Car)", "Rolling Resistance", "Incline Force", "Air Resistance"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopLeft, + } + }, + { + xAccessor: velocityAccessor, + yAccessor: [totalExternalLoadTorqueAtSecondaryAccessor, rollingResistanceTorqueAtSecondaryAccessor, inclineTorqueAtSecondaryAccessor, dragTorqueAtSecondaryAccessor], + config: { + title: "External Load Torques at Secondary vs Vehicle Speed", + xAxis: { name: "Vehicle Speed", type: "value", unit: getAxisUnit(velocityAccessor) }, + yAxis: { name: "Torque", type: "value", unit: getAxisUnit(totalExternalLoadTorqueAtSecondaryAccessor) }, + seriesNames: ["Total (Secondary)", "Rolling Resistance", "Incline Torque", "Air Resistance Torque"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopLeft, + } + } + ] }, { - xAccessor: engineRpmAccessor, - yAccessor: [engineTorqueAccessor, primary_tau_upperAccessor], - config: { - title: "Engine Torque and Primary Upper Bound vs Engine RPM", - xAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, - yAxis: { name: "Torque", type: "value", unit: getAxisUnit(engineTorqueAccessor) }, - seriesNames: ["Engine Torque", "Primary Upper Bound"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopLeft, - } - }, -]}, -{ - title: "Pulley Forces (Overall)", - graphs: [ - /** PRIM AND SEC OVERALL GRAPHS */ - { // Shows overall direction of shift - xAccessor: timeAccessor, - yAccessor: [primaryAxialForceTotalAccessor, secondaryAxialForceTotalAccessor], - config: { - title: "Pulley Total Axial Forces vs Time", - xAxis: { name: "Time", type: "value", unit: "s" }, - yAxis: { name: "Axial Force", type: "value", unit: "N" }, - seriesNames: ["Primary Total", "Secondary Total"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } - }, - { // Breakdown of axial clamping and belt centrifugal contribution - xAccessor: timeAccessor, - yAccessor: [primaryAxialClampingForceAccessor, secondaryAxialClampingForceAccessor, primaryAxialCentrifugalFromBeltAccessor, secondaryAxialCentrifugalFromBeltAccessor], - config: { - title: "Axial Force Breakdown vs Time", - xAxis: { name: "Time", type: "value", unit: "s" }, - yAxis: { name: "Force", type: "value", unit: "N" }, - seriesNames: ["Primary Clamping", "Secondary Clamping", "Primary Belt Centrifugal", "Secondary Belt Centrifugal"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } + title: "CVT Ratio", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [cvtRatioAccessor], + config: { + title: "CVT Ratio vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "CVT Ratio", type: "value", unit: getAxisUnit(cvtRatioAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [cvtRatioRateOfChangeAccessor], + config: { + title: "CVT Ratio Rate of Change vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "CVT Ratio Rate of Change", type: "value", unit: getAxisUnit(cvtRatioRateOfChangeAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [primaryOuterRadiusRateAccessor, secondaryOuterRadiusRateAccessor], + config: { + title: "Primary and Secondary Outer Radius Rate vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Radius Rate", type: "value", unit: getAxisUnit(primaryOuterRadiusRateAccessor) }, + seriesNames: ["Primary Radius Rate", "Secondary Radius Rate"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: velocityAccessor, + yAccessor: [engineRpmAccessor], + config: { + title: "Shift Curve (Engine RPM vs Vehicle Speed)", + xAxis: { name: "Vehicle Speed", type: "value", unit: getAxisUnit(velocityAccessor) }, + yAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + } + ] }, - // cvtAccelerationAccessor { - xAccessor: timeAccessor, - yAccessor: [cvtAccelerationAccessor], - config: { - title: "CVT Acceleration vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "CVT Acceleration", type: "value", unit: getAxisUnit(cvtAccelerationAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } - }, -]}, -{ - title: "Primary Pulley", - graphs: [ - /** PRIMARY GRAPHS */ - { // Primary axial force breakdown into components - xAccessor: timeAccessor, - yAccessor: [primaryAxialClampingForceAccessor, primaryFlyweightForceAccessor, primarySpringForceAccessor], - config: { - title: "Primary Axial Forces vs Time", - xAxis: { name: "Time", type: "value", unit: "s" }, - yAxis: { name: "Force", type: "value", unit: "N" }, - seriesNames: ["Net", "Flyweight", "Spring"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } - }, - { // Visualize how much ramp is doing(raw vs post ramp) - xAccessor: engineRpmAccessor, - yAccessor: [rawFlyweightCentrifugalForce, primaryFlyweightForceAccessor], - config: { - title: "Ramp Impact (Raw vs Post-Ramp) vs Engine RPM", - xAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, - yAxis: { name: "Force", type: "value", unit: "N" }, - seriesNames: ["Raw Flyweight", "Flyweight"], - showXLine: true, - showYLine: true, - tooltipPosition: TooltipPosition.TopLeft, - } + title: "Engine", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [engineRpmAccessor], + config: { + title: "Engine RPM vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [engineTorqueAccessor], + config: { + title: "Engine Torque vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Engine Torque", type: "value", unit: getAxisUnit(engineTorqueAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [enginePowerAccessor], + config: { + title: "Engine Power vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Engine Power", type: "value", unit: getAxisUnit(enginePowerAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: engineRpmAccessor, + yAccessor: [engineTorqueAccessor, primary_tau_upperAccessor], + config: { + title: "Engine Torque and Primary Upper Bound vs Engine RPM", + xAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, + yAxis: { name: "Torque", type: "value", unit: getAxisUnit(engineTorqueAccessor) }, + seriesNames: ["Engine Torque", "Primary Upper Bound"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopLeft, + } + } + ] }, - // primaryRampAngleAccessor { - xAccessor: timeAccessor, - yAccessor: [primaryRampAngleAccessor], - config: { - title: "Primary Ramp Angle vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Ramp Angle", type: "value", unit: "degrees" }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - }, -]}, -{ - title: "Secondary Pulley", - graphs: [ - /** SECONDARY GRAPHS */ - { // Top level breakdown of axial from helix and axial from spring - xAccessor: timeAccessor, - yAccessor: [secondaryAxialClampingForceAccessor, secondaryHelixForceAccessor, secondarySpringCompForceAccessor], - config: { - title: "Secondary Axial Forces vs Time", - xAxis: { name: "Time", type: "value", unit: "s" }, - yAxis: { name: "Secondary Force", type: "value", unit: "N" }, - seriesNames: ["Net", "Helix Force", "Spring Comp Force"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - }, - { // Torques that go into the helix - xAccessor: timeAccessor, - yAccessor: [secondaryHelixFeedbackTorqueAccessor, secondaryHelixSpringTorqueAccessor], - config: { - title: "Secondary Torques vs Time", - xAxis: { name: "Time", type: "value", unit: "s" }, - yAxis: { name: "Torque", type: "value", unit: "N·m" }, - seriesNames: ["Reactive Feedback", "Torsional Spring"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - }, - { // Same graph as 2 above, but vs CVT ratio - xAccessor: cvtRatioAccessor, - yAccessor: [secondaryAxialClampingForceAccessor, secondaryHelixForceAccessor, secondarySpringCompForceAccessor], - config: { - title: "Secondary Axial Forces vs CVT RATIO", - xAxis: { name: "CVT RATIO", type: "value", unit: getAxisUnit(cvtRatioAccessor) }, - yAxis: { name: "Secondary Force", type: "value", unit: "N" }, - seriesNames: ["Net", "Helix Force", "Spring Comp Force"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopLeft, - } - }, - { // Same graph as 2 above, but vs CVT ratio - xAccessor: cvtRatioAccessor, - yAccessor: [secondaryHelixFeedbackTorqueAccessor, secondaryHelixSpringTorqueAccessor], - config: { - title: "Secondary Torques vs CVT RATIO", - xAxis: { name: "CVT RATIO", type: "value", unit: getAxisUnit(cvtRatioAccessor) }, - yAxis: { name: "Torque", type: "value", unit: "N·m" }, - seriesNames: ["Reactive Feedback", "Torsional Spring"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopLeft, - } - }, -]}, -{ - title: "Slip Model", - graphs: [ - /** SLIP MODEL GRAPHS */ - { // Per-pulley torque bounds - xAccessor: timeAccessor, - yAccessor: [ - primary_tau_upperAccessor, - primary_tau_lowerAccessor, - secondary_tau_positiveAccessor, - secondary_tau_negativeAccessor - ], - config: { - title: "Primary and Secondary Torque Bounds vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Torque", type: "value", unit: getAxisUnit(coupling_torqueAccessor) }, - seriesNames: [ - "Primary Upper Bound", - "Primary Lower Bound", - "Secondary Upper Bound", - "Secondary Lower Bound" - ], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } - }, - { // Overall bounds and torque tracking - xAccessor: timeAccessor, - yAccessor: [tau_upperAccessor, tau_lowerAccessor, coupling_torqueAccessor, torque_demandAccessor], - config: { - title: "Overall Bounds, Coupling, and No-Slip Torque vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Torque", type: "value", unit: "N·m" }, - seriesNames: ["Overall Upper Bound", "Overall Lower Bound", "Coupling (Final)", "No-Slip Torque"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } + title: "Pulley Forces (Overall)", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [primaryAxialForceTotalAccessor, secondaryAxialForceTotalAccessor], + config: { + title: "Pulley Total Axial Forces vs Time", + xAxis: { name: "Time", type: "value", unit: "s" }, + yAxis: { name: "Axial Force", type: "value", unit: "N" }, + seriesNames: ["Primary Total", "Secondary Total"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [primaryAxialClampingForceAccessor, secondaryAxialClampingForceAccessor, primaryAxialCentrifugalFromBeltAccessor, secondaryAxialCentrifugalFromBeltAccessor], + config: { + title: "Axial Force Breakdown vs Time", + xAxis: { name: "Time", type: "value", unit: "s" }, + yAxis: { name: "Force", type: "value", unit: "N" }, + seriesNames: ["Primary Clamping", "Secondary Clamping", "Primary Belt Centrifugal", "Secondary Belt Centrifugal"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [cvtAccelerationAccessor], + config: { + title: "CVT Acceleration vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "CVT Acceleration", type: "value", unit: getAxisUnit(cvtAccelerationAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + } + ] }, { - xAccessor: timeAccessor, - yAccessor: [relativeVelocityAccessor], - config: { - title: "Relative Velocity vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Relative Velocity", type: "value", unit: getAxisUnit(relativeVelocityAccessor) }, - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } - } -]}, -{ - title: "Belt Debug", - graphs: [ - { - xAccessor: timeAccessor, - yAccessor: [primaryBeltSpeedAccessor, secondaryBeltSpeedAccessor, beltSpeedAccessor, beltTargetSpeedAccessor], - config: { - title: "Belt Speeds vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Speed", type: "value", unit: getAxisUnit(beltSpeedAccessor) }, - seriesNames: ["Primary Belt Speed", "Secondary Belt Speed", "Transport Speed v_b", "Target Speed v_b*"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.BottomRight, - } + title: "Primary Pulley", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [primaryAxialClampingForceAccessor, primaryFlyweightForceAccessor, primarySpringForceAccessor], + config: { + title: "Primary Axial Forces vs Time", + xAxis: { name: "Time", type: "value", unit: "s" }, + yAxis: { name: "Force", type: "value", unit: "N" }, + seriesNames: ["Net", "Flyweight", "Spring"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: engineRpmAccessor, + yAccessor: [rawFlyweightCentrifugalForce, primaryFlyweightForceAccessor], + config: { + title: "Ramp Impact (Raw vs Post-Ramp) vs Engine RPM", + xAxis: { name: "Engine RPM", type: "value", unit: getAxisUnit(engineRpmAccessor) }, + yAxis: { name: "Force", type: "value", unit: "N" }, + seriesNames: ["Raw Flyweight", "Flyweight"], + showXLine: true, + showYLine: true, + tooltipPosition: TooltipPosition.TopLeft, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [primaryRampAngleAccessor], + config: { + title: "Primary Ramp Angle vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Ramp Angle", type: "value", unit: "degrees" }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + } + ] }, { - xAccessor: timeAccessor, - yAccessor: [relativeVelocityAccessor, beltCompatibleSpeedAccessor, beltSpeedDerivativeAccessor], - config: { - title: "Belt Relative Speed and Dynamics vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Belt Diagnostics", type: "value", unit: getAxisUnit(relativeVelocityAccessor) }, - seriesNames: ["Relative Speed (v_p - v_s)", "Compatible Speed", "v_b_dot"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } + title: "Secondary Pulley", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [secondaryAxialClampingForceAccessor, secondaryHelixForceAccessor, secondarySpringCompForceAccessor], + config: { + title: "Secondary Axial Forces vs Time", + xAxis: { name: "Time", type: "value", unit: "s" }, + yAxis: { name: "Secondary Force", type: "value", unit: "N" }, + seriesNames: ["Net", "Helix Force", "Spring Comp Force"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [secondaryHelixFeedbackTorqueAccessor, secondaryHelixSpringTorqueAccessor], + config: { + title: "Secondary Torques vs Time", + xAxis: { name: "Time", type: "value", unit: "s" }, + yAxis: { name: "Torque", type: "value", unit: "N·m" }, + seriesNames: ["Reactive Feedback", "Torsional Spring"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: cvtRatioAccessor, + yAccessor: [secondaryAxialClampingForceAccessor, secondaryHelixForceAccessor, secondarySpringCompForceAccessor], + config: { + title: "Secondary Axial Forces vs CVT RATIO", + xAxis: { name: "CVT RATIO", type: "value", unit: getAxisUnit(cvtRatioAccessor) }, + yAxis: { name: "Secondary Force", type: "value", unit: "N" }, + seriesNames: ["Net", "Helix Force", "Spring Comp Force"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopLeft, + } + }, + { + xAccessor: cvtRatioAccessor, + yAccessor: [secondaryHelixFeedbackTorqueAccessor, secondaryHelixSpringTorqueAccessor], + config: { + title: "Secondary Torques vs CVT RATIO", + xAxis: { name: "CVT RATIO", type: "value", unit: getAxisUnit(cvtRatioAccessor) }, + yAxis: { name: "Torque", type: "value", unit: "N·m" }, + seriesNames: ["Reactive Feedback", "Torsional Spring"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopLeft, + } + } + ] }, { - xAccessor: timeAccessor, - yAccessor: [beltIsStickAccessor, isSlippingAccessor, beltRelaxationTimeAccessor], - config: { - title: "Belt Mode Flags and Relaxation Time vs Time", - xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, - yAxis: { name: "Mode / Time Constant", type: "value", unit: getAxisUnit(beltRelaxationTimeAccessor) }, - seriesNames: ["is_stick (belt)", "is_slipping (slip model)", "T_b"], - showXLine: true, - showYLine: false, - tooltipPosition: TooltipPosition.TopRight, - } + title: "Slip Model", + graphs: [ + { + xAccessor: timeAccessor, + yAccessor: [coupling_torqueAccessor, primary_tau_upperAccessor, primary_tau_lowerAccessor, torque_demandAccessor], + config: { + title: "Primary Torque and Bounds vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Torque", type: "value", unit: getAxisUnit(coupling_torqueAccessor) }, + seriesNames: ["tau_p", "Primary Upper Bound", "Primary Lower Bound", "tau_p_ns"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [appliedSecondaryTorqueAccessor, secondary_tau_positiveAccessor, secondary_tau_negativeAccessor, secondaryTorqueDemandAccessor], + config: { + title: "Secondary Torque and Bounds vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Torque", type: "value", unit: getAxisUnit(appliedSecondaryTorqueAccessor) }, + seriesNames: ["tau_s", "Secondary Upper Bound", "Secondary Lower Bound", "tau_s_ns"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [branchNoSlipAccessor, branchPrimarySlipAccessor, branchSecondarySlipAccessor, branchBothSlipAccessor], + config: { + title: "Slip Branch State vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Branch Active", type: "value", unit: getAxisUnit(branchNoSlipAccessor) }, + seriesNames: ["No Slip", "Primary Slip", "Secondary Slip", "Both Slip"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [primaryRelativeVelocityAccessor, secondaryRelativeVelocityAccessor], + config: { + title: "Primary and Secondary Relative Velocity vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Relative Velocity", type: "value", unit: getAxisUnit(primaryRelativeVelocityAccessor) }, + seriesNames: ["Primary Relative Velocity", "Secondary Relative Velocity"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [beltSpeedAccessor], + config: { + title: "Belt Speed vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Belt Speed", type: "value", unit: getAxisUnit(beltSpeedAccessor) }, + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.TopRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [appliedPrimaryTorqueAccessor, appliedSecondaryTorqueAccessor], + config: { + title: "Applied Contact Torque vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Applied Torque", type: "value", unit: getAxisUnit(appliedPrimaryTorqueAccessor) }, + seriesNames: ["Applied tau_p", "Applied tau_s"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + }, + { + xAccessor: timeAccessor, + yAccessor: [primaryAngularAccelerationAccessor, secondaryAngularAccelerationAccessor], + config: { + title: "Primary and Secondary Angular Acceleration vs Time", + xAxis: { name: "Time", type: "value", unit: getAxisUnit(timeAccessor) }, + yAxis: { name: "Angular Acceleration", type: "value", unit: getAxisUnit(primaryAngularAccelerationAccessor) }, + seriesNames: ["Primary Angular Acceleration", "Secondary Angular Acceleration"], + showXLine: true, + showYLine: false, + tooltipPosition: TooltipPosition.BottomRight, + } + } + ] } -]} ]; // Flatten categories into single array for backward compatibility -export const graphConfigs: GraphConfig[] = graphCategories.flatMap(category => category.graphs); \ No newline at end of file +export const graphConfigs: GraphConfig[] = graphCategories.flatMap(category => category.graphs); diff --git a/frontend/src/utils/conversion/timeStepDataConversion.ts b/frontend/src/utils/conversion/timeStepDataConversion.ts index bfc18443..32f04c86 100644 --- a/frontend/src/utils/conversion/timeStepDataConversion.ts +++ b/frontend/src/utils/conversion/timeStepDataConversion.ts @@ -1,218 +1,374 @@ /** - * Unit conversion utilities for simulation time step data. + * Unit conversion utilities for simulation analysis data. */ import type { components } from '@types'; -import { convertValue, getTargetUnit, type UnitConfiguration, DEFAULT_UNIT_CONFIG } from './unitConversion'; +import { + convertValue, + getTargetUnit, + type UnitConfiguration, + DEFAULT_UNIT_CONFIG, +} from './unitConversion'; -type FormattedSimulationResultModel = components['schemas']['FormattedSimulationResultModel']; -type TimeStepDataModel = components['schemas']['TimeStepDataModel']; +type AnalysisStepDataModel = components['schemas']['AnalysisStepDataModel']; +type SimulationAnalysisResultModel = components['schemas']['SimulationAnalysisResultModel']; +type ContactDynamicsBreakdownModel = components['schemas']['ContactDynamicsBreakdownModel']; +type CVTGeometryResultModel = components['schemas']['CVTGeometryResultModel']; +type ContactTorqueResultModel = components['schemas']['ContactTorqueResultModel']; +type DrivetrainAccelerationBreakdownModel = components['schemas']['DrivetrainAccelerationBreakdownModel']; +type CvtDynamicsBreakdownModel = components['schemas']['CvtDynamicsBreakdownModel']; +type PulleyForcesModel = components['schemas']['PulleyForcesModel']; +type PrimaryForceBreakdownModel = components['schemas']['PrimaryForceBreakdownModel']; +type SecondaryForceBreakdownModel = components['schemas']['SecondaryForceBreakdownModel']; +type EngineTorqueBreakdownModel = components['schemas']['EngineTorqueBreakdownModel']; +type ExternalLoadForceBreakdownModel = components['schemas']['ExternalLoadForceBreakdownModel']; +type NoSlipBreakdownModel = components['schemas']['NoSlipBreakdownModel']; +type NoSlipResultModel = components['schemas']['NoSlipResultModel']; +type TorqueAdmissibilityResultModel = components['schemas']['TorqueAdmissibilityResultModel']; +type PrimaryTorqueAdmissibilityBreakdownModel = components['schemas']['PrimaryTorqueAdmissibilityBreakdownModel']; +type SecondaryTorqueAdmissibilityBreakdownModel = components['schemas']['SecondaryTorqueAdmissibilityBreakdownModel']; +type SlipMetricsResultModel = components['schemas']['SlipMetricsResultModel']; -// Convert a single time step's data -function convertTimeStepData( - timeStep: TimeStepDataModel, - config: UnitConfiguration -): TimeStepDataModel { - // Helper to convert values with the simplified config - const conv = (value: number, type: T) => +function convFactory(config: UnitConfiguration) { + return (value: number, type: T) => convertValue(value, type, getTargetUnit(type, config)); +} +function convertPrimaryForceBreakdown( + breakdown: PrimaryForceBreakdownModel, + config: UnitConfiguration +): PrimaryForceBreakdownModel { + const conv = convFactory(config); return { - time: conv(timeStep.time, 'time'), - - state: { - shift_distance: conv(timeStep.state.shift_distance, 'distance'), - shift_velocity: conv(timeStep.state.shift_velocity, 'velocity'), - primary_pulley_angular_velocity: conv(timeStep.state.primary_pulley_angular_velocity, 'angular_velocity'), - secondary_pulley_angular_velocity: conv(timeStep.state.secondary_pulley_angular_velocity, 'angular_velocity'), - v_b: conv(timeStep.state.v_b, 'velocity'), - }, - derived_state: { - car_velocity: conv(timeStep.derived_state.car_velocity, 'velocity'), - car_position: conv(timeStep.derived_state.car_position, 'distance'), - engine_angular_velocity: conv(timeStep.derived_state.engine_angular_velocity, 'angular_velocity'), - engine_angular_position: conv(timeStep.derived_state.engine_angular_position, 'angle'), - }, - drivetrain: { - belt_slip: ({ - coupling_torque: conv(timeStep.drivetrain.belt_slip.coupling_torque, 'torque'), - torque_demand: conv(timeStep.drivetrain.belt_slip.torque_demand, 'torque'), - tau_upper: conv(timeStep.drivetrain.belt_slip.tau_upper, 'torque'), - tau_lower: conv(timeStep.drivetrain.belt_slip.tau_lower, 'torque'), - primary_tau_bounds: { - tau_lower: conv(timeStep.drivetrain.belt_slip.primary_tau_bounds.tau_lower, 'torque'), - tau_upper: conv(timeStep.drivetrain.belt_slip.primary_tau_bounds.tau_upper, 'torque'), - numerator: { - clamping_term: conv(timeStep.drivetrain.belt_slip.primary_tau_bounds.numerator.clamping_term, 'torque'), - load_term: conv(timeStep.drivetrain.belt_slip.primary_tau_bounds.numerator.load_term, 'torque'), - shift_term: conv(timeStep.drivetrain.belt_slip.primary_tau_bounds.numerator.shift_term, 'torque'), - net: conv(timeStep.drivetrain.belt_slip.primary_tau_bounds.numerator.net, 'torque'), - }, - denominator_upper: { - inverse_radius_term: timeStep.drivetrain.belt_slip.primary_tau_bounds.denominator_upper.inverse_radius_term, - inertial_feedback_term: timeStep.drivetrain.belt_slip.primary_tau_bounds.denominator_upper.inertial_feedback_term, - net: timeStep.drivetrain.belt_slip.primary_tau_bounds.denominator_upper.net, - }, - denominator_lower: { - inverse_radius_term: timeStep.drivetrain.belt_slip.primary_tau_bounds.denominator_lower.inverse_radius_term, - inertial_feedback_term: timeStep.drivetrain.belt_slip.primary_tau_bounds.denominator_lower.inertial_feedback_term, - net: timeStep.drivetrain.belt_slip.primary_tau_bounds.denominator_lower.net, - }, - }, - secondary_tau_bounds: { - tau_negative: conv(timeStep.drivetrain.belt_slip.secondary_tau_bounds.tau_negative, 'torque'), - tau_positive: conv(timeStep.drivetrain.belt_slip.secondary_tau_bounds.tau_positive, 'torque'), - numerator: { - spring_term: conv(timeStep.drivetrain.belt_slip.secondary_tau_bounds.numerator.spring_term, 'torque'), - load_term: conv(timeStep.drivetrain.belt_slip.secondary_tau_bounds.numerator.load_term, 'torque'), - shift_term: conv(timeStep.drivetrain.belt_slip.secondary_tau_bounds.numerator.shift_term, 'torque'), - net: conv(timeStep.drivetrain.belt_slip.secondary_tau_bounds.numerator.net, 'torque'), - }, - denominator_positive: { - inverse_radius_term: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_positive.inverse_radius_term, - helix_feedback_term: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_positive.helix_feedback_term, - inertial_feedback_term: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_positive.inertial_feedback_term, - net: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_positive.net, - }, - denominator_negative: { - inverse_radius_term: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_negative.inverse_radius_term, - helix_feedback_term: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_negative.helix_feedback_term, - inertial_feedback_term: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_negative.inertial_feedback_term, - net: timeStep.drivetrain.belt_slip.secondary_tau_bounds.denominator_negative.net, - }, - }, - effective_cvt_ratio_time_derivative: conv(timeStep.drivetrain.belt_slip.effective_cvt_ratio_time_derivative, 'dimensionless_rate'), - is_slipping: timeStep.drivetrain.belt_slip.is_slipping - } as TimeStepDataModel['drivetrain']['belt_slip']), - belt_state: { - is_stick: timeStep.drivetrain.belt_state.is_stick, - relative_speed: conv(timeStep.drivetrain.belt_state.relative_speed, 'velocity'), - primary_belt_speed: conv(timeStep.drivetrain.belt_state.primary_belt_speed, 'velocity'), - secondary_belt_speed: conv(timeStep.drivetrain.belt_state.secondary_belt_speed, 'velocity'), - v_b_star: conv(timeStep.drivetrain.belt_state.v_b_star, 'velocity'), - T_b: conv(timeStep.drivetrain.belt_state.T_b, 'time'), - v_b: conv(timeStep.drivetrain.belt_state.v_b, 'velocity'), - v_b_compatible: conv(timeStep.drivetrain.belt_state.v_b_compatible, 'velocity'), - v_b_dot: conv(timeStep.drivetrain.belt_state.v_b_dot, 'acceleration'), + flyweightForce: { + radius: conv(breakdown.flyweightForce.radius, 'distance'), + angular_velocity: conv(breakdown.flyweightForce.angular_velocity, 'angular_velocity'), + angle: conv(breakdown.flyweightForce.angle, 'angle'), + centrifugal_force: conv(breakdown.flyweightForce.centrifugal_force, 'force'), + angle_multiplier: breakdown.flyweightForce.angle_multiplier, + net: conv(breakdown.flyweightForce.net, 'force'), }, - primary_pulley: { - primary_pulley_drive_torque: conv(timeStep.drivetrain.primary_pulley.primary_pulley_drive_torque, 'torque'), - coupling_torque_at_primary_pulley: conv(timeStep.drivetrain.primary_pulley.coupling_torque_at_primary_pulley, 'torque'), - power: conv(timeStep.drivetrain.primary_pulley.power, 'power'), - primary_pulley_angular_velocity: conv(timeStep.drivetrain.primary_pulley.primary_pulley_angular_velocity, 'angular_velocity'), - primary_pulley_angular_acceleration: conv(timeStep.drivetrain.primary_pulley.primary_pulley_angular_acceleration, 'angular_acceleration') + springForce: { + compression: conv(breakdown.springForce.compression, 'distance'), + net: conv(breakdown.springForce.net, 'force'), }, - secondary_pulley: { - coupling_torque_at_secondary_pulley: conv(timeStep.drivetrain.secondary_pulley.coupling_torque_at_secondary_pulley, 'torque'), - external_load_torque_at_secondary_pulley: conv(timeStep.drivetrain.secondary_pulley.external_load_torque_at_secondary_pulley, 'torque'), - external_forces: { - rolling_resistance_force: conv(timeStep.drivetrain.secondary_pulley.external_forces.rolling_resistance_force, 'force'), - incline_force: conv(timeStep.drivetrain.secondary_pulley.external_forces.incline_force, 'force'), - drag_force: conv(timeStep.drivetrain.secondary_pulley.external_forces.drag_force, 'force'), - net_force_at_car: conv(timeStep.drivetrain.secondary_pulley.external_forces.net_force_at_car, 'force'), - rolling_resistance_torque_at_secondary: conv(timeStep.drivetrain.secondary_pulley.external_forces.rolling_resistance_torque_at_secondary, 'torque'), - incline_torque_at_secondary: conv(timeStep.drivetrain.secondary_pulley.external_forces.incline_torque_at_secondary, 'torque'), - drag_torque_at_secondary: conv(timeStep.drivetrain.secondary_pulley.external_forces.drag_torque_at_secondary, 'torque'), - net_torque_at_secondary: conv(timeStep.drivetrain.secondary_pulley.external_forces.net_torque_at_secondary, 'torque') - }, - secondary_pulley_angular_acceleration: conv(timeStep.drivetrain.secondary_pulley.secondary_pulley_angular_acceleration, 'angular_acceleration') + net: conv(breakdown.net, 'force'), + }; +} + +function convertSecondaryForceBreakdown( + breakdown: SecondaryForceBreakdownModel, + config: UnitConfiguration +): SecondaryForceBreakdownModel { + const conv = convFactory(config); + return { + springCompForce: { + compression: conv(breakdown.springCompForce.compression, 'distance'), + net: conv(breakdown.springCompForce.net, 'force'), }, - cvt_dynamics: { - primaryPulleyState: { - forces: { - axial_clamping_force: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.forces.axial_clamping_force, 'force'), - axial_centrifugal_from_belt: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.forces.axial_centrifugal_from_belt, 'force'), - axial_force_total: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.forces.axial_force_total, 'force'), - }, - wrap_angle: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.wrap_angle, 'angle'), - radius: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.radius, 'distance'), - angular_velocity: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.angular_velocity, 'angular_velocity'), - angular_position: conv(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.angular_position, 'angle'), - breakdown: { - ...convertPulleyForce(timeStep.drivetrain.cvt_dynamics.primaryPulleyState.breakdown, config) - } + helix_force: { + feedbackTorque: conv(breakdown.helix_force.feedbackTorque, 'torque'), + springTorque: { + rotation: conv(breakdown.helix_force.springTorque.rotation, 'angle'), + net: conv(breakdown.helix_force.springTorque.net, 'torque'), }, - secondaryPulleyState: { - forces: { - axial_clamping_force: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.forces.axial_clamping_force, 'force'), - axial_centrifugal_from_belt: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.forces.axial_centrifugal_from_belt, 'force'), - axial_force_total: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.forces.axial_force_total, 'force'), - }, - wrap_angle: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.wrap_angle, 'angle'), - radius: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.radius, 'distance'), - angular_velocity: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.angular_velocity, 'angular_velocity'), - angular_position: conv(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.angular_position, 'angle'), - breakdown: { - ...convertPulleyForce(timeStep.drivetrain.cvt_dynamics.secondaryPulleyState.breakdown, config) - } - }, - friction: conv(timeStep.drivetrain.cvt_dynamics.friction, 'dimensionless'), - acceleration: conv(timeStep.drivetrain.cvt_dynamics.acceleration, 'acceleration'), - cvt_ratio: conv(timeStep.drivetrain.cvt_dynamics.cvt_ratio, 'dimensionless'), - net: conv(timeStep.drivetrain.cvt_dynamics.net, 'force') - } - } -}; + angle: conv(breakdown.helix_force.angle, 'angle'), + radius: conv(breakdown.helix_force.radius, 'distance'), + angle_multiplier: breakdown.helix_force.angle_multiplier, + net: conv(breakdown.helix_force.net, 'force'), + }, + net: conv(breakdown.net, 'force'), + }; } -// Convert primary or secondary pulley force (needed to handle the union type) -function convertPulleyForce( - pulleyForce: components['schemas']['PrimaryForceBreakdownModel'] | components['schemas']['SecondaryForceBreakdownModel'], +function convertPulleyForces( + pulleyForces: PulleyForcesModel, config: UnitConfiguration -): components['schemas']['PrimaryForceBreakdownModel'] | components['schemas']['SecondaryForceBreakdownModel'] { - const conv = (value: number, type: T) => - convertValue(value, type, getTargetUnit(type, config)); +): PulleyForcesModel { + const conv = convFactory(config); + const pulleyBreakdown = pulleyForces.pulley_breakdown; + return { + pulley_breakdown: + 'flyweightForce' in pulleyBreakdown + ? convertPrimaryForceBreakdown(pulleyBreakdown, config) + : convertSecondaryForceBreakdown(pulleyBreakdown, config), + belt_wrap: { + wrap_angle: conv(pulleyForces.belt_wrap.wrap_angle, 'angle'), + axial_belt_force: conv(pulleyForces.belt_wrap.axial_belt_force, 'force'), + }, + net: conv(pulleyForces.net, 'force'), + }; +} - // Type guard: check if it's PrimaryForceBreakdownModel - if ('flyweightForce' in pulleyForce) { - return { - flyweightForce: { - radius: conv(pulleyForce.flyweightForce.radius, 'distance'), - angular_velocity: conv(pulleyForce.flyweightForce.angular_velocity, 'angular_velocity'), - angle: conv(pulleyForce.flyweightForce.angle, 'angle'), - centrifugal_force: conv(pulleyForce.flyweightForce.centrifugal_force, 'force'), - angle_multiplier: pulleyForce.flyweightForce.angle_multiplier, // dimensionless - net: conv(pulleyForce.flyweightForce.net, 'force'), - }, - springForce: { - compression: conv(pulleyForce.springForce.compression, 'distance'), - net: conv(pulleyForce.springForce.net, 'force'), - }, - net: conv(pulleyForce.net, 'force'), - }; - } else { - // It's SecondaryForceBreakdownModel - return { - springCompForce: { - compression: conv(pulleyForce.springCompForce.compression, 'distance'), - net: conv(pulleyForce.springCompForce.net, 'force'), - }, - helix_force: { - feedbackTorque: conv(pulleyForce.helix_force.feedbackTorque, 'torque'), - springTorque: { - rotation: conv(pulleyForce.helix_force.springTorque.rotation, 'angle'), - net: conv(pulleyForce.helix_force.springTorque.net, 'torque'), - }, - angle: conv(pulleyForce.helix_force.angle, 'angle'), - radius: conv(pulleyForce.helix_force.radius, 'distance'), - angle_multiplier: pulleyForce.helix_force.angle_multiplier, // dimensionless - net: conv(pulleyForce.helix_force.net, 'force'), - }, - net: conv(pulleyForce.net, 'force'), - }; - } +function convertEngineBreakdown( + breakdown: EngineTorqueBreakdownModel, + config: UnitConfiguration +): EngineTorqueBreakdownModel { + const conv = convFactory(config); + return { + engine_torque: conv(breakdown.engine_torque, 'torque'), + engine_speed: conv(breakdown.engine_speed, 'angular_velocity'), + engine_power: conv(breakdown.engine_power, 'power'), + }; +} + +function convertExternalLoadBreakdown( + breakdown: ExternalLoadForceBreakdownModel, + config: UnitConfiguration +): ExternalLoadForceBreakdownModel { + const conv = convFactory(config); + return { + rolling_resistance_force: conv(breakdown.rolling_resistance_force, 'force'), + incline_force: conv(breakdown.incline_force, 'force'), + drag_force: conv(breakdown.drag_force, 'force'), + net_force_at_car: conv(breakdown.net_force_at_car, 'force'), + rolling_resistance_torque_at_secondary: conv( + breakdown.rolling_resistance_torque_at_secondary, + 'torque' + ), + incline_torque_at_secondary: conv(breakdown.incline_torque_at_secondary, 'torque'), + drag_torque_at_secondary: conv(breakdown.drag_torque_at_secondary, 'torque'), + net_torque_at_secondary: conv(breakdown.net_torque_at_secondary, 'torque'), + }; +} + +function convertNoSlipBreakdown( + breakdown: NoSlipBreakdownModel, + config: UnitConfiguration +): NoSlipBreakdownModel { + const conv = convFactory(config); + return { + r_p: conv(breakdown.r_p, 'distance'), + r_s: conv(breakdown.r_s, 'distance'), + r_p_dot: conv(breakdown.r_p_dot, 'distance'), + r_s_dot: conv(breakdown.r_s_dot, 'distance'), + tau_engine_over_r_p: conv(breakdown.tau_engine_over_r_p, 'force'), + tau_load_over_r_s: conv(breakdown.tau_load_over_r_s, 'force'), + primary_inertia_term: conv(breakdown.primary_inertia_term, 'force'), + secondary_inertia_term: conv(breakdown.secondary_inertia_term, 'force'), + numerator: conv(breakdown.numerator, 'force'), + denominator: breakdown.denominator, + }; +} + +function convertPrimaryTorqueAdmissibilityBreakdown( + breakdown: PrimaryTorqueAdmissibilityBreakdownModel, + config: UnitConfiguration +): PrimaryTorqueAdmissibilityBreakdownModel { + const conv = convFactory(config); + return { + shift_distance: conv(breakdown.shift_distance, 'distance'), + wrap_angle: conv(breakdown.wrap_angle, 'angle'), + effective_radius: conv(breakdown.effective_radius, 'distance'), + centroid_radius: conv(breakdown.centroid_radius, 'distance'), + centroid_radius_rate: conv(breakdown.centroid_radius_rate, 'velocity'), + axial_clamping_force: conv(breakdown.axial_clamping_force, 'force'), + belt_centripetal_term: conv(breakdown.belt_centripetal_term, 'force'), + friction_coefficient: breakdown.friction_coefficient, + sheave_half_angle: conv(breakdown.sheave_half_angle, 'angle'), + tau_p_stick_limit: conv(breakdown.tau_p_stick_limit, 'torque'), + tau_p_stick_upper: conv(breakdown.tau_p_stick_upper, 'torque'), + tau_p_stick_lower: conv(breakdown.tau_p_stick_lower, 'torque'), + }; +} + +function convertSecondaryTorqueAdmissibilityBreakdown( + breakdown: SecondaryTorqueAdmissibilityBreakdownModel, + config: UnitConfiguration +): SecondaryTorqueAdmissibilityBreakdownModel { + const conv = convFactory(config); + return { + shift_distance: conv(breakdown.shift_distance, 'distance'), + wrap_angle: conv(breakdown.wrap_angle, 'angle'), + effective_radius: conv(breakdown.effective_radius, 'distance'), + centroid_radius: conv(breakdown.centroid_radius, 'distance'), + centroid_radius_rate: conv(breakdown.centroid_radius_rate, 'velocity'), + helix_rotation: conv(breakdown.helix_rotation, 'angle'), + helix_rotation_rate: conv(breakdown.helix_rotation_rate, 'angle'), + spring_torsion_term: conv(breakdown.spring_torsion_term, 'torque'), + spring_comp_term: conv(breakdown.spring_comp_term, 'force'), + belt_centripetal_term: conv(breakdown.belt_centripetal_term, 'force'), + friction_coefficient: breakdown.friction_coefficient, + sheave_half_angle: conv(breakdown.sheave_half_angle, 'angle'), + denominator_upper: breakdown.denominator_upper, + denominator_lower: breakdown.denominator_lower, + tau_stick_upper: conv(breakdown.tau_stick_upper, 'torque'), + tau_stick_lower: conv(breakdown.tau_stick_lower, 'torque'), + }; +} + +function convertTorqueAdmissibilityResult( + admissibility: TorqueAdmissibilityResultModel, + config: UnitConfiguration +): TorqueAdmissibilityResultModel { + return { + primary: convertPrimaryTorqueAdmissibilityBreakdown(admissibility.primary, config), + secondary: convertSecondaryTorqueAdmissibilityBreakdown(admissibility.secondary, config), + primary_tau_p_stick_upper: convFactory(config)(admissibility.primary_tau_p_stick_upper, 'torque'), + primary_tau_p_stick_lower: convFactory(config)(admissibility.primary_tau_p_stick_lower, 'torque'), + secondary_tau_stick_upper: convFactory(config)(admissibility.secondary_tau_stick_upper, 'torque'), + secondary_tau_stick_lower: convFactory(config)(admissibility.secondary_tau_stick_lower, 'torque'), + }; +} + +function convertSlipMetricsResult( + slipMetrics: SlipMetricsResultModel, + config: UnitConfiguration +): SlipMetricsResultModel { + const conv = convFactory(config); + return { + primary_relative_speed: conv(slipMetrics.primary_relative_speed, 'velocity'), + secondary_relative_speed: conv(slipMetrics.secondary_relative_speed, 'velocity'), + primary_slip_direction: slipMetrics.primary_slip_direction, + secondary_slip_direction: slipMetrics.secondary_slip_direction, + primary_admissible: slipMetrics.primary_admissible, + secondary_admissible: slipMetrics.secondary_admissible, + admissibility: convertTorqueAdmissibilityResult(slipMetrics.admissibility, config), + no_slip: convertNoSlipResult(slipMetrics.no_slip, config), + }; +} + +function convertNoSlipResult( + noSlip: NoSlipResultModel, + config: UnitConfiguration +): NoSlipResultModel { + const conv = convFactory(config); + return { + v_b_dot_ns: conv(noSlip.v_b_dot_ns, 'acceleration'), + tau_p_ns: conv(noSlip.tau_p_ns, 'torque'), + tau_s_ns: conv(noSlip.tau_s_ns, 'torque'), + breakdown: convertNoSlipBreakdown(noSlip.breakdown, config), + }; +} + +function convertContactTorqueResult( + contact: ContactTorqueResultModel, + config: UnitConfiguration +): ContactTorqueResultModel { + const conv = convFactory(config); + return { + tau_p: conv(contact.tau_p, 'torque'), + tau_s: conv(contact.tau_s, 'torque'), + branch: contact.branch, + slip_metrics: convertSlipMetricsResult(contact.slip_metrics, config), + branch_result: { + branch: contact.branch_result.branch, + tau_p: conv(contact.branch_result.tau_p, 'torque'), + tau_s: conv(contact.branch_result.tau_s, 'torque'), + }, + }; +} + +function convertDrivetrainAccelerationBreakdown( + drivetrain: DrivetrainAccelerationBreakdownModel, + config: UnitConfiguration +): DrivetrainAccelerationBreakdownModel { + const conv = convFactory(config); + return { + ω_p_dot: conv(drivetrain.ω_p_dot, 'angular_acceleration'), + ω_s_dot: conv(drivetrain.ω_s_dot, 'angular_acceleration'), + v_b_dot: conv(drivetrain.v_b_dot, 'acceleration'), + engine_breakdown: convertEngineBreakdown(drivetrain.engine_breakdown, config), + external_load_breakdown: convertExternalLoadBreakdown(drivetrain.external_load_breakdown, config), + tau_p: conv(drivetrain.tau_p, 'torque'), + tau_s: conv(drivetrain.tau_s, 'torque'), + }; +} + +function convertCvtDynamicsBreakdown( + shift: CvtDynamicsBreakdownModel, + config: UnitConfiguration +): CvtDynamicsBreakdownModel { + const conv = convFactory(config); + return { + primaryPulleyState: convertPulleyForces(shift.primaryPulleyState, config), + secondaryPulleyState: convertPulleyForces(shift.secondaryPulleyState, config), + friction: conv(shift.friction, 'force'), + acceleration: conv(shift.acceleration, 'acceleration'), + net: conv(shift.net, 'force'), + }; +} + +function convertContactBreakdown( + contactBreakdown: ContactDynamicsBreakdownModel, + config: UnitConfiguration +): ContactDynamicsBreakdownModel { + return { + contact: convertContactTorqueResult(contactBreakdown.contact, config), + drivetrain: convertDrivetrainAccelerationBreakdown(contactBreakdown.drivetrain, config), + shift: convertCvtDynamicsBreakdown(contactBreakdown.shift, config), + geometry: convertGeometryResult(contactBreakdown.geometry, config), + }; +} + +function convertGeometryResult( + geometry: CVTGeometryResultModel, + config: UnitConfiguration +): CVTGeometryResultModel { + const conv = convFactory(config); + return { + effective_cvt_ratio: geometry.effective_cvt_ratio, + effective_cvt_ratio_rate_of_change: conv( + geometry.effective_cvt_ratio_rate_of_change, + 'dimensionless_rate' + ), + primary_outer_radius: conv(geometry.primary_outer_radius, 'distance'), + primary_effective_radius: conv(geometry.primary_effective_radius, 'distance'), + primary_centroid_radius: conv(geometry.primary_centroid_radius, 'distance'), + primary_radius_rate_of_change: conv(geometry.primary_radius_rate_of_change, 'velocity'), + secondary_outer_radius: conv(geometry.secondary_outer_radius, 'distance'), + secondary_effective_radius: conv(geometry.secondary_effective_radius, 'distance'), + secondary_centroid_radius: conv(geometry.secondary_centroid_radius, 'distance'), + secondary_radius_rate_of_change: conv(geometry.secondary_radius_rate_of_change, 'velocity'), + primary_wrap_angle: conv(geometry.primary_wrap_angle, 'angle'), + secondary_wrap_angle: conv(geometry.secondary_wrap_angle, 'angle'), + }; +} + +function convertDerivedState( + derivedState: AnalysisStepDataModel['derived_state'], + config: UnitConfiguration +): AnalysisStepDataModel['derived_state'] { + const conv = convFactory(config); + return { + car_velocity: conv(derivedState.car_velocity, 'velocity'), + car_position: conv(derivedState.car_position, 'distance'), + belt_position: conv(derivedState.belt_position, 'distance'), + engine_angular_velocity: conv(derivedState.engine_angular_velocity, 'angular_velocity'), + engine_angular_position: conv(derivedState.engine_angular_position, 'angle'), + }; +} + +// Convert a single time step's data +function convertTimeStepData( + timeStep: AnalysisStepDataModel, + config: UnitConfiguration +): AnalysisStepDataModel { + const conv = convFactory(config); + + return { + time: conv(timeStep.time, 'time'), + state: { + s: conv(timeStep.state.s, 'distance'), + s_dot: conv(timeStep.state.s_dot, 'velocity'), + ω_p: conv(timeStep.state.ω_p, 'angular_velocity'), + ω_s: conv(timeStep.state.ω_s, 'angular_velocity'), + v_b: conv(timeStep.state.v_b, 'velocity'), + }, + derived_state: convertDerivedState(timeStep.derived_state, config), + contact_breakdown: convertContactBreakdown(timeStep.contact_breakdown, config), + }; } // Main conversion function for simulation results export function convertSimulationData( - data: FormattedSimulationResultModel, + data: SimulationAnalysisResultModel, config: UnitConfiguration = DEFAULT_UNIT_CONFIG -): FormattedSimulationResultModel { - const conv = (value: number, type: T) => - convertValue(value, type, getTargetUnit(type, config)); +): SimulationAnalysisResultModel { + const conv = convFactory(config); return { - data: data.data.map(timeStep => convertTimeStepData(timeStep, config)), + data: data.data.map((timeStep) => convertTimeStepData(timeStep, config)), termination: { ...data.termination, final_time: conv(data.termination.final_time, 'time'),