Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions backend/app/models/auto_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
get_origin,
get_type_hints,
)
import enum
from pydantic import BaseModel, ConfigDict, create_model

_CACHE: dict[type, type[BaseModel]] = {}
Expand All @@ -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

Expand Down
31 changes: 22 additions & 9 deletions cvtModel/src/cvt_simulator/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions cvtModel/src/cvt_simulator/constants/car_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,15 @@ 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)

@computed_field
@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)

Expand Down
54 changes: 54 additions & 0 deletions cvtModel/src/cvt_simulator/core/components/belt_wrap.py
Original file line number Diff line number Diff line change
@@ -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)

30 changes: 30 additions & 0 deletions cvtModel/src/cvt_simulator/core/components/engine.py
Original file line number Diff line number Diff line change
@@ -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(ω) * ω
91 changes: 91 additions & 0 deletions cvtModel/src/cvt_simulator/core/components/primary_pulley.py
Original file line number Diff line number Diff line change
@@ -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)

119 changes: 119 additions & 0 deletions cvtModel/src/cvt_simulator/core/components/secondary_pulley.py
Original file line number Diff line number Diff line change
@@ -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)

Loading
Loading