├── .gitignore ├── LICENSE ├── README.md ├── code ├── opentire │ ├── Core │ │ ├── TIRFile.py │ │ ├── __init__.py │ │ └── tirestate.py │ ├── TireModel │ │ ├── PAC2002 │ │ │ ├── PAC2002.py │ │ │ ├── PAC2002_Core.py │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── solvermode.py │ │ └── tiremodelbase.py │ ├── __init__.py │ └── opentire.py ├── setup.py └── testEnv.py └── examples ├── FY_SA_Example.ipynb ├── Moment_Diagram_Example.ipynb └── SA_Sweep_Example.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 OpenTire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTIRE - The open-source mathematical tire modelling library 2 | 3 | The OpenTire project is a non-commercial project that has three primary goals: 4 | * Provide a library of open-source tire models in an easily accessible and useable format 5 | * Provide a technical platform for collaborative tire model development 6 | * Build a library of tire data and tire models available for use in research projects 7 | 8 | ## Background 9 | The complexity involved in simulating tires can make it difficult to implement tire models in research and development studies. Firstly, turning equations in literature into software code can be a daunting task. Testing and validating an implementation is difficult and time consuming. Secondly, without a single open-source implementation of tire models collaborative development of tire models are difficult and slow. Lastly, getting access to tire data and tire models without large investments in testing is very difficult. As a result a significant part of tire and vehicle dynamics research is carried out using a tire model for 205/60R15 tire, which is readily available in literature. 10 | 11 | ## Getting Started 12 | If you are unfamiliar with packages and modules in Python, the first thing you'll have to do before using OpenTire is to install the package with your Python installation. To help with this, OpenTire comes with a setup scripts that automates it all. To install it, run the setup.py script with install as an argument. 13 | 14 | ```python 15 | setup.py install 16 | ``` 17 | Once OpenTire is installed, you can load up a tire model with these commands: 18 | 19 | ```python 20 | from opentire import OpenTire 21 | from opentire.Core import TireState 22 | 23 | openTire = OpenTire() 24 | myTireModel = openTire.createmodel('PAC2002') 25 | 26 | state = TireState() 27 | state['FZ'] = 1500 28 | state['IA'] = 0.0 29 | state['SR'] = 0.0 30 | state['SA'] = 0.0 31 | state['FY'] = 0.0 32 | state['V'] = 10.0 33 | state['P'] = 260000 34 | 35 | myTireModel.solve(state) 36 | ``` 37 | 38 | For more comprehensive examples, please check out the additional examples in the examples folder. 39 | 40 | ## Examples 41 | To help you get started using OpenTire, there are a number of different Jupyter Notebooks which demonstrates how to initiate OpenTire and how to integrate into a tire or vehicle simulation. 42 | 43 | ## More info 44 | Existing tire model implementations are generally built on dated programming languages which make integration with modern software tools difficult and inefficient. By utilizing a modern open-source scientific programming language (Python) along with modern collaborative tools (online code repositories) the threshold for entry into developing and utilizing tire models is significantly lowered. 45 | 46 | ## Development plan 47 | The first release includes implementations of commonly used tire models along with implementation examples, benchmark studies and the first library of parameterized tire models. 48 | 49 | Since the project start in late 2014, the project has received support from both academic and commercial contributors. More contributors are wanted to help define the roadmap for future development. 50 | -------------------------------------------------------------------------------- /code/opentire/Core/TIRFile.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | __author__ = 'henningo' 4 | 5 | from opentire import OpenTire 6 | import os 7 | 8 | class TIRFile(): 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.template_file = 'TIR' 12 | self.tire_model = None 13 | self.Coefficients = dict() 14 | self.Descriptions = dict() 15 | self.Comments = "" 16 | 17 | def load(self, fname, output=False): 18 | 19 | # Load a TIR-file and build a dictionary of parameters 20 | f = open(fname, 'r') 21 | tir_data = f.readlines() 22 | f.close() 23 | 24 | for line in tir_data: 25 | if "=" in line: # This means we have a parameter on the line 26 | name, value, description = self.ProcessParameterLine(line) 27 | self.Coefficients[name] = value 28 | self.Descriptions[name] = description 29 | 30 | elif "!" in line: # This we have a comment 31 | self.Comments += line 32 | 33 | 34 | if output is True: 35 | print(self.Comments) 36 | print(self.Coefficients) 37 | 38 | # Create the model based on type in TIR file 39 | opentire = OpenTire() 40 | self.Coefficients['PROPERTY_FILE_FORMAT'] = self.Coefficients['PROPERTY_FILE_FORMAT'].replace("'", "") 41 | print(self.Coefficients['PROPERTY_FILE_FORMAT']) 42 | tm = opentire.createmodel(self.Coefficients['PROPERTY_FILE_FORMAT']) 43 | print(tm) 44 | 45 | if tm == None: 46 | print("Model could not be recognized") 47 | return None 48 | 49 | # Assign parameters to the model 50 | for parameter_name in self.Coefficients: # Loop over the parameters we found 51 | 52 | if parameter_name in tm.Coefficients: # See if it exists in tire model 53 | tm.Coefficients[parameter_name] = float(self.Coefficients[parameter_name]) # Assign the value 54 | 55 | else: 56 | if output is True: 57 | print("Could not map parameter: " + str(parameter_name)) 58 | 59 | return tm 60 | 61 | def ProcessParameterLine(self, linedata): 62 | 63 | parameter_name = "Parameter" 64 | parameter_value = "0.0" 65 | parameter_description = "No Description Available" 66 | 67 | # We need to extract the parameter name, the value and the description 68 | linedata = linedata.split("=") 69 | parameter_name = linedata[0] 70 | 71 | if "$" in linedata[1]: # Means we have a comment 72 | parameter_value = linedata[1].split("$")[0] 73 | parameter_description = linedata[1].split("$")[1] 74 | 75 | else: 76 | parameter_value = linedata[1] 77 | 78 | # Return line data with leading/trailing spaces "stripped" (.strip()) 79 | return parameter_name.strip(), parameter_value.strip(), parameter_description.strip() 80 | 81 | def save(self, tire_model, fname, overwrite_file=True): 82 | 83 | if overwrite_file is True: # This means we need to start from a template 84 | 85 | # Find what type of tire model 86 | model_type = tire_model.ModelInfo['Name'] 87 | subfolder = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 88 | template_filename = subfolder + '\\Templates\\' + model_type + '.tir' 89 | print("Using TIR template") 90 | 91 | else: # Load the resulting file 92 | # TODO: check if it is a real TIR file 93 | template_filename = fname 94 | 95 | # Load a TIR-file and build a dictionary of parameters 96 | f = open(template_filename, 'r') 97 | tir_data = f.readlines() 98 | print(len(tir_data)) 99 | f.close() 100 | 101 | f = open(fname, 'wb') 102 | 103 | for line in tir_data: 104 | if "=" in line: # This means we have a parameter on the line 105 | name, value, description = self.ProcessParameterLine(line) 106 | if name in tire_model.Coefficients: 107 | value = tire_model.Coefficients[name] 108 | 109 | # Then we have to re-create the line, using somewhat standard TIR layout 110 | new_line = name.ljust(25) + '= ' + str(value) 111 | new_line = new_line.ljust(50) + '$' + description + '\n' 112 | 113 | f.writelines(new_line) 114 | 115 | else: # We should probably write this line as well, even though we don't have this coefficient 116 | f.writelines(line) 117 | 118 | else: # This means it is a normal line, we will write it 119 | f.writelines(line) 120 | f.close() 121 | -------------------------------------------------------------------------------- /code/opentire/Core/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | from opentire.Core.tirestate import TireState 4 | from opentire.Core.TIRFile import TIRFile 5 | -------------------------------------------------------------------------------- /code/opentire/Core/tirestate.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | class TireState(dict): 4 | 5 | def __init__(self, *args, **kwargs): 6 | self['FX'] = 0.0 7 | self['FY'] = 0.0 8 | self['FZ'] = 0.0 9 | self['MX'] = 0.0 10 | self['MY'] = 0.0 11 | self['MZ'] = 0.0 12 | self['SA'] = 0.0 13 | self['SR'] = 0.0 14 | self['IA'] = 0.0 15 | -------------------------------------------------------------------------------- /code/opentire/TireModel/PAC2002/PAC2002.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | from ..tiremodelbase import TireModelBase 3 | from ..solvermode import SolverMode 4 | import math 5 | import numpy as np 6 | from opentire.TireModel.PAC2002.PAC2002_Core import PAC2002_Core 7 | 8 | class PAC2002(TireModelBase): 9 | 10 | def createmodel(self): 11 | self.ModelInfo = dict() 12 | self.Coefficients = dict() 13 | 14 | self.ModelInfo['Name'] = 'PAC2002' 15 | self.ModelInfo['Description'] = 'An implementation of Pacejka 2002 as described in the First Editions of Tire and Vehicle Dynamics' 16 | 17 | # Create an instance for internal calcs 18 | # These are typically called multiple times in the code 19 | self.Core = PAC2002_Core() 20 | 21 | # General Coefficients 22 | self.Coefficients['FNOMIN'] = 4850 23 | self.Coefficients['UNLOADED_RADIUS'] = 0.344 24 | self.Coefficients['LONGVL'] = 16.6 25 | 26 | # General Scaling Factors 27 | self.Coefficients['LFZ0'] = 1.0 28 | self.Coefficients['LCZ'] = 1.0 29 | 30 | # Pure Longitudinal Scaling Factors 31 | self.Coefficients['LCX'] = 1.0 32 | self.Coefficients['LMUX'] = 1.0 33 | self.Coefficients['LEX'] = 1.0 34 | self.Coefficients['LKX'] = 1.0 35 | self.Coefficients['LHX'] = 1.0 36 | self.Coefficients['LVX'] = 1.0 37 | self.Coefficients['LGAX'] = 1.0 38 | 39 | # Pure Lateral Scaling Factors 40 | self.Coefficients['LCY'] = 1.0 41 | self.Coefficients['LMUY'] = 1.0 42 | self.Coefficients['LEY'] = 1.0 43 | self.Coefficients['LKY'] = 1.0 44 | self.Coefficients['LHY'] = 1.0 45 | self.Coefficients['LVY'] = 1.0 46 | self.Coefficients['LGAY'] = 1.0 47 | 48 | # Pure Aligning Moment Scaling Factors 49 | self.Coefficients['LTR'] = 1.0 50 | self.Coefficients['LRES'] = 1.0 51 | self.Coefficients['LGAZ'] = 1.0 52 | 53 | # Combined Scaling Factors 54 | self.Coefficients['LXAL'] = 1.0 55 | self.Coefficients['LYKA'] = 1.0 56 | self.Coefficients['LVYKA'] = 1.0 57 | self.Coefficients['LS'] = 1.0 58 | 59 | # Overturning Scaling Factors 60 | self.Coefficients['LMX'] = 1.0 61 | self.Coefficients['LVMX'] = 1.0 62 | 63 | # Rolling Resistance Factors 64 | self.Coefficients['LMY'] = 1.0 65 | 66 | #Relaxation Scaling Factors 67 | self.Coefficients['LSGKP'] = 1.0 68 | self.Coefficients['LSGAL'] = 1.0 69 | 70 | # Pure Lateral Coefficients 71 | self.Coefficients['PCY1'] = 1.3507 72 | self.Coefficients['PDY1'] = 1.0489 73 | self.Coefficients['PDY2'] = -0.18033 74 | self.Coefficients['PDY3'] = -2.8821 75 | self.Coefficients['PEY1'] = -0.0074722 76 | self.Coefficients['PEY2'] = -0.0063208 77 | self.Coefficients['PEY3'] = -9.9935 78 | self.Coefficients['PEY4'] = -760.14 79 | self.Coefficients['PKY1'] = -21.92 80 | self.Coefficients['PKY2'] = 2.0012 81 | self.Coefficients['PKY3'] = -0.024778 82 | self.Coefficients['PHY1'] = 0.0026747 83 | self.Coefficients['PHY2'] = 8.9094e-005 84 | self.Coefficients['PHY3'] = 0.031415 85 | self.Coefficients['PVY1'] = 0.037318 86 | self.Coefficients['PVY2'] = -0.010049 87 | self.Coefficients['PVY3'] = -0.32931 88 | self.Coefficients['PVY4'] = -0.69553 89 | 90 | # Combined Lateral Coefficients 91 | self.Coefficients['RBY1'] = 7.1433 92 | self.Coefficients['RBY2'] = 9.1916 93 | self.Coefficients['RBY3'] = -0.027856 94 | self.Coefficients['RCY1'] = 1.0719 95 | self.Coefficients['REY1'] = -0.27572 96 | self.Coefficients['REY2'] = 0.32802 97 | self.Coefficients['RHY1'] = 5.7448e-006 98 | self.Coefficients['RHY2'] = -3.1368e-005 99 | self.Coefficients['RVY1'] = -0.027825 100 | self.Coefficients['RVY2'] = 0.053604 101 | self.Coefficients['RVY3'] = -0.27568 102 | self.Coefficients['RVY4'] = 12.12 103 | self.Coefficients['RVY5'] = 1.9 104 | self.Coefficients['RVY6'] = -10.704 105 | 106 | # Pure Aligning Torque Coefficients 107 | self.Coefficients['QBZ1'] = 10.904 108 | self.Coefficients['QBZ2'] = -1.8412 109 | self.Coefficients['QBZ3'] = -0.52041 110 | self.Coefficients['QBZ4'] = 0.039211 111 | self.Coefficients['QBZ5'] = 0.41511 112 | self.Coefficients['QBZ9'] = 8.9846 113 | self.Coefficients['QBZ10'] = 0.0 114 | self.Coefficients['QCZ1'] = 1.2136 115 | self.Coefficients['QDZ1'] = 0.093509 116 | self.Coefficients['QDZ2'] = -0.009218 117 | self.Coefficients['QDZ3'] = -0.057061 118 | self.Coefficients['QDZ4'] = 0.73954 119 | self.Coefficients['QDZ6'] = -0.0067783 120 | self.Coefficients['QDZ7'] = 0.0052254 121 | self.Coefficients['QDZ8'] = -0.18175 122 | self.Coefficients['QDZ9'] = 0.029952 123 | self.Coefficients['QEZ1'] = -1.5697 124 | self.Coefficients['QEZ2'] = 0.33394 125 | self.Coefficients['QEZ3'] = 0.0 126 | self.Coefficients['QEZ4'] = 0.26711 127 | self.Coefficients['QEZ5'] = -3.594 128 | self.Coefficients['QHZ1'] = 0.0047326 129 | self.Coefficients['QHZ2'] = 0.0026687 130 | self.Coefficients['QHZ3'] = 0.11998 131 | self.Coefficients['QHZ4'] = 0.059083 132 | 133 | # Combined Aligning Coefficients 134 | self.Coefficients['SSZ1'] = 0.033372 135 | self.Coefficients['SSZ2'] = 0.0043624 136 | self.Coefficients['SSZ3'] = 0.56742 137 | self.Coefficients['SSZ4'] = -0.24116 138 | 139 | # Pure longitudinal coefficients 140 | self.Coefficients['PCX1'] = 1.6411 141 | self.Coefficients['PDX1'] = 1.1739 142 | self.Coefficients['PDX2'] = -0.16395 143 | self.Coefficients['PDX3'] = 5.0 144 | self.Coefficients['PEX1'] = 0.46403 145 | self.Coefficients['PEX2'] = 0.25022 146 | self.Coefficients['PEX3'] = 0.067842 147 | self.Coefficients['PEX4'] = -3.7604e-005 148 | self.Coefficients['PKX1'] = 22.303 149 | self.Coefficients['PKX2'] = 0.48896 150 | self.Coefficients['PKX3'] = 0.21253 151 | self.Coefficients['PHX1'] = 0.0012297 #TODO: There is something funky with these params. Should be zero? To have no offset at SR=0 152 | self.Coefficients['PHX2'] = 0.0004318 153 | self.Coefficients['PVX1'] = -8.8098e-006 154 | self.Coefficients['PVX2'] = 1.862e-005 155 | 156 | # Combined longitudinal coefficients 157 | self.Coefficients['RBX1'] = 13.276 158 | self.Coefficients['RBX2'] = -13.778 159 | self.Coefficients['RCX1'] = 1.2568 160 | self.Coefficients['REX1'] = 0.65225 161 | self.Coefficients['REX2'] = -0.24948 162 | self.Coefficients['RHX1'] = 0.0050722 163 | 164 | # Overturning Moment Coefficients 165 | self.Coefficients['QSX1'] = 2.3155e-04 166 | self.Coefficients['QSX2'] = 0.51574 167 | self.Coefficients['QSX3'] = 0.046399 168 | 169 | # Rolling Resistance Coefficients 170 | self.Coefficients['QSY1'] = 0.01 171 | self.Coefficients['QSY2'] = 0.0 172 | self.Coefficients['QSY3'] = 0.0 173 | self.Coefficients['QSY4'] = 0.0 174 | 175 | # Loaded Radius 176 | self.Coefficients['QV1'] = 7.15073791e-005 177 | self.Coefficients['QV2'] = 0.14892 178 | self.Coefficients['QFCX'] = 0 179 | self.Coefficients['QFCY'] = 0 180 | self.Coefficients['QFCG'] = -3.0 181 | self.Coefficients['QFZ1'] = 28.0249 182 | self.Coefficients['QFZ2'] = 29.2 183 | 184 | # Rolling Radius 185 | self.Coefficients['BREFF'] = 8.4 186 | self.Coefficients['DREFF'] = 0.27 187 | self.Coefficients['FREFF'] = 0.07 188 | 189 | # Lateral Relaxation 190 | self.Coefficients['PTY1'] = 2.1439 191 | self.Coefficients['PTY2'] = 1.9829 192 | 193 | # Longitudinal Relaxation 194 | self.Coefficients['PTX1'] = 2.3657 195 | self.Coefficients['PTX2'] = 1.4112 196 | self.Coefficients['PTX3'] = 0.56626 197 | 198 | # Turn-slip and parking calculated values. 199 | # Note that turn slip isn't implemented yet 200 | # Therefore all these reduction factors are set to 1 201 | # For future versions these needs to be calcualted for every time 202 | self.ZETA0 = 1 203 | self.ZETA1 = 1 204 | self.ZETA2 = 1 205 | self.ZETA3 = 1 206 | self.ZETA4 = 1 207 | self.ZETA5 = 1 208 | self.ZETA6 = 1 209 | self.ZETA7 = 1 210 | self.ZETA8 = 1 211 | 212 | def save(self, fname, data): 213 | return 'saving' 214 | 215 | def load(self, fname): 216 | return 'loading' 217 | 218 | def solve(self, state, mode=SolverMode.All): 219 | 220 | if mode is SolverMode.PureFy or mode is SolverMode.PureMz: 221 | state['FY'] = self.calculate_pure_fy(state) 222 | 223 | if mode is SolverMode.Fy or mode is SolverMode.All: 224 | state['FY'] = self.calculate_fy(state) 225 | 226 | if mode is SolverMode.PureFx: 227 | state['FX'] = self.calculate_pure_fx(state) 228 | 229 | if mode is SolverMode.Fx or mode is SolverMode.All: 230 | state['FX'] = self.calculate_fx(state) 231 | 232 | if mode is SolverMode.PureMz: 233 | state['MZ'] = self.calculate_mz(state) 234 | 235 | if mode is SolverMode.Mz or mode is SolverMode.All: 236 | state['MZ'] = self.calculate_mz(state) 237 | 238 | if mode is SolverMode.Mx or mode is SolverMode.All: 239 | state['MX'] = self.calculate_mx(state) 240 | 241 | if mode is SolverMode.Mz or mode is SolverMode.All: 242 | state['MY'] = self.calculate_my(state) 243 | 244 | if mode is SolverMode.Radius or mode is SolverMode.All: 245 | state['RL'], state['RE'] = self.calculate_radius(state) 246 | 247 | if mode is SolverMode.Relaxation or mode is SolverMode.All: 248 | state['SIGMA_ALPHA'] = self.calculate_lateral_relaxation_length(state) 249 | state['SIGMA_KAPPA'] = self.calculate_longitudinal_relaxation_length(state) 250 | 251 | 252 | return state 253 | 254 | def getparameters(self): 255 | return self.Coefficients 256 | 257 | def setparameters(self, params): 258 | #TODO: Must check that params keys actually match the models coefs. 259 | self.Coefficients = params 260 | 261 | return True # should return False if the parameter structure doesn't match required one 262 | 263 | def getmodelinfo(self): 264 | return self.ModelInfo 265 | 266 | ###Private properties 267 | 268 | def calculate_common_values(self, state): 269 | # First we calculate some standard values used in multiple locations 270 | dfz = (state['FZ'] - self.Coefficients['FNOMIN']) / self.Coefficients['FNOMIN'] 271 | alpha_star = math.tan(state['SA']) * np.sign(state['V']) 272 | gamma_star = math.sin(state['IA']) 273 | kappa = state['SR'] 274 | 275 | return dfz, alpha_star, gamma_star, kappa 276 | 277 | def calculate_fx(self, state): 278 | p = self.Coefficients 279 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 280 | 281 | 282 | F_x0 = self.calculate_pure_fx(state) 283 | 284 | S_Hxa = self.Core.calculate_S_Hxa(p) 285 | 286 | #60 287 | alpha_s = alpha_star + S_Hxa 288 | 289 | B_xa = self.Core.calculate_B_xa(p, kappa) 290 | C_xa = self.Core.calculate_C_xa(p) 291 | E_xa = self.Core.calculate_E_xa(p, dfz) 292 | 293 | # 66 294 | G_xa_numerator = math.cos(C_xa * math.atan(B_xa * alpha_s - E_xa * (B_xa * alpha_s - math.atan(B_xa * alpha_s)))) 295 | G_xa_denominator = math.cos(C_xa * math.atan(B_xa * S_Hxa - E_xa * (B_xa * S_Hxa - math.atan(B_xa * S_Hxa)))) 296 | G_xa = G_xa_numerator / G_xa_denominator 297 | 298 | return F_x0 * G_xa 299 | 300 | def calculate_pure_fx(self, state): 301 | p = self.Coefficients 302 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 303 | 304 | C_x = self.Core.calculate_C_x(p) 305 | D_x = self.Core.calculate_D_x(p, state, dfz, gamma_star, self.ZETA1) 306 | S_Hx = self.Core.calculate_S_Hx(p, dfz) 307 | 308 | # 19 309 | kappa_x = kappa + S_Hx 310 | 311 | E_x = self.Core.calculate_E_x(p, dfz, kappa_x) 312 | K_x = self.Core.calculate_K_x(p, state, dfz) 313 | B_x = self.Core.calculate_B_x(C_x, D_x, K_x) 314 | S_Vx = self.Core.calculate_S_Vx(p, state, dfz, self.ZETA1) 315 | 316 | # 18 317 | fx_pure = D_x * math.sin((C_x * math.atan(B_x * kappa_x - E_x * (B_x * kappa_x - math.atan(B_x * kappa_x))))) + S_Vx 318 | 319 | return fx_pure 320 | 321 | def calculate_fy(self, state): 322 | p = self.Coefficients 323 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 324 | 325 | F_y0 = self.calculate_pure_fy(state) 326 | 327 | gamma_y = self.Core.calculate_gamma_y(p, gamma_star) 328 | B_yk = self.Core.calculate_B_yk(p, alpha_star) 329 | C_yk = self.Core.calculate_C_yk(p) 330 | E_yk = self.Core.calculate_E_yk(p, dfz) 331 | 332 | D_Vyk = self.Core.calculate_D_Vyk(p, state, dfz, gamma_y, alpha_star, gamma_star) 333 | S_Vyk = self.Core.calculate_S_Vyk(p, kappa, D_Vyk) 334 | 335 | # 69 336 | S_Hyk = self.Core.calculate_S_Hyk(p, dfz) 337 | kappa_s = kappa + S_Hyk 338 | 339 | # 77 340 | G_yk_numerator = math.cos(C_yk * math.atan(B_yk * kappa_s - E_yk * (B_yk * kappa_s - math.atan(B_yk * kappa_s)))) 341 | G_yk_denominator = math.cos(C_yk * math.atan(B_yk * S_Hyk - E_yk * (B_yk * S_Hyk - math.atan(B_yk * S_Hyk)))) 342 | G_yk = G_yk_numerator/G_yk_denominator 343 | 344 | return G_yk * F_y0 + S_Vyk 345 | 346 | def calculate_pure_fy(self, state): 347 | p = self.Coefficients 348 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 349 | 350 | gamma_y = self.Core.calculate_gamma_y(p, gamma_star) 351 | 352 | C_y = self.Core.calculate_C_y(p) 353 | D_y = self.Core.calculate_D_y(p, state, dfz, gamma_y, self.ZETA1) 354 | S_Hy = self.Core.calculate_S_Hy(p, dfz, gamma_y, self.ZETA0, self.ZETA4) 355 | 356 | # 31 357 | alpha_y = alpha_star + S_Hy 358 | 359 | E_y = self.Core.calculate_E_y(p, dfz, gamma_y, alpha_y) 360 | K_y = self.Core.calculate_K_y(p, state, gamma_y, self.ZETA3) 361 | B_y = self.Core.calculate_B_y(C_y, D_y, K_y) 362 | S_Vy = self.Core.calculate_S_Vy(p, state, dfz, gamma_y, self.ZETA4) 363 | 364 | # 30 365 | fy_pure = D_y * math.sin(C_y * math.atan(B_y * alpha_y - E_y * (B_y * alpha_y - math.atan(B_y * alpha_y)))) + S_Vy 366 | 367 | return fy_pure 368 | 369 | def calculate_mz(self, state): 370 | p = self.Coefficients 371 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 372 | 373 | # 32 374 | gamma_y = self.Core.calculate_gamma_y(p, gamma_star) 375 | gamma_z = self.Core.calculate_gamma_z(p, gamma_star) 376 | 377 | # Combined Trail Calcs 378 | S_Ht = self.Core.calculate_S_Ht(p, dfz, gamma_z) 379 | 380 | # 47 381 | alpha_t = alpha_star + S_Ht 382 | K_x = self.Core.calculate_K_x(p, state, dfz) 383 | K_y = self.Core.calculate_K_y(p, state, gamma_y, self.ZETA3) 384 | 385 | # 84 386 | alpha_teq = math.atan(math.sqrt(math.tan(alpha_t)**2 + (K_x/K_y)**2 * kappa**2) * np.sign(alpha_t)) 387 | B_t = self.Core.calculate_B_t(p, dfz, gamma_z) 388 | C_t = self.Core.calculate_C_t(p) 389 | D_t = self.Core.calculate_D_t(p, state, dfz, gamma_z, self.ZETA5) 390 | E_t = self.Core.calculate_E_t(p, dfz, gamma_z, alpha_t, B_t, C_t) 391 | 392 | # Combined Trail Calc, here we are using alpha_teq instead of alpha_t 393 | t = self.Core.calculate_t(p, B_t, C_t, D_t, E_t, alpha_teq, alpha_star) 394 | 395 | # Combined Residual Torque Calcs 396 | S_Hy = self.Core.calculate_S_Hy(p, dfz, gamma_y, self.ZETA0, self.ZETA4) 397 | S_Vy = self.Core.calculate_S_Vy(p, state, dfz, gamma_y, self.ZETA4) 398 | K_y = self.Core.calculate_K_y(p, state, gamma_y, self.ZETA3) 399 | S_Hf = self.Core.calculate_S_Hf(S_Hy, S_Vy, K_y) 400 | 401 | # 47 402 | alpha_r = alpha_star + S_Hf 403 | 404 | # 85 405 | alpha_req = math.atan(math.sqrt(math.tan(alpha_r)**2 + (K_x/K_y)**2 * kappa**2) * np.sign(alpha_r)) 406 | 407 | C_y = self.Core.calculate_C_y(p) 408 | D_y = self.Core.calculate_D_y(p, state, dfz, gamma_y, self.ZETA1) 409 | B_y = self.Core.calculate_B_y(C_y, D_y, K_y) 410 | B_r = self.Core.calculate_B_r(p, B_y, C_y, self.ZETA6) 411 | C_r = self.Core.calculate_C_r(self.ZETA7) 412 | D_r = self.Core.calculate_D_r(p, state, dfz, gamma_z, self.ZETA8) 413 | 414 | M_zr = self.Core.calculate_M_zr(B_r, C_r, D_r, alpha_req, alpha_star) 415 | 416 | # FY Prime Calcs 417 | D_Vyk = self.Core.calculate_D_Vyk(p, state, dfz, gamma_y, alpha_star, gamma_star) 418 | S_Vyk = self.Core.calculate_S_Vyk(p, kappa, D_Vyk) 419 | 420 | Fy_prime = state['FY'] - S_Vyk # This is the combined lateral force without Fx induced Fy 421 | 422 | # Pneumatic scrub (s) calcs 423 | s = p['UNLOADED_RADIUS'] * (p['SSZ1'] + p['SSZ2'] * (state['FY'] / (p['FNOMIN'] * p['LFZ0'])) + (p['SSZ3'] + p['SSZ4'] * dfz) * gamma_star) * p['LS'] 424 | 425 | M_prime = -t * Fy_prime + M_zr + s * state['FX'] 426 | 427 | return M_prime 428 | 429 | def calculate_pure_mz(self, state): 430 | p = self.Coefficients 431 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 432 | 433 | gamma_z = self.Core.calculate_gamma_z(p, gamma_star) 434 | gamma_y = self.Core.calculate_gamma_y(p, gamma_star) 435 | 436 | # Trail calculations (D_t, C_t, B_t, E_t) 437 | S_Ht = self.Core.calculate_S_Ht(p, dfz, gamma_z) 438 | 439 | # 47 440 | alpha_t = alpha_star + S_Ht 441 | 442 | B_t = self.Core.calculate_B_t(p, dfz, gamma_z) 443 | C_t = self.Core.calculate_C_t(p) 444 | D_t = self.Core.calculate_D_t(p, state, dfz, gamma_z, self.ZETA5) 445 | E_t = self.Core.calculate_E_t(p, dfz, gamma_z, alpha_t, B_t, C_t) 446 | 447 | # Trail Calculation 448 | t = self.Core.calculate_t(p, B_t, C_t, D_t, E_t, alpha_t, alpha_star) 449 | 450 | # Residual Moment Calculations (C_r, D_r, B_r) 451 | # These calcs uses Pure Fy calculations, so they are repeated here (such as K_y and D_y) 452 | S_Hy = self.Core.calculate_S_Hy(p, dfz, gamma_y, self.ZETA0, self.ZETA4) 453 | S_Vy = self.Core.calculate_S_Vy(p, state, dfz, gamma_y, self.ZETA4) 454 | K_y = self.Core.calculate_K_y(p, state, gamma_y, self.ZETA3) 455 | C_y = self.Core.calculate_C_y(p) 456 | D_y = self.Core.calculate_D_y(p, state, dfz, gamma_y, self.ZETA1) 457 | B_y = self.Core.calculate_B_y(C_y, D_y, K_y) 458 | 459 | B_r = self.Core.calculate_B_r(p, B_y, C_y, self.ZETA6) 460 | C_r = self.Core.calculate_C_r(self.ZETA7) 461 | D_r = self.Core.calculate_D_r(p, state, dfz, gamma_z, self.ZETA8) 462 | S_Hf = self.Core.calculate_S_Hf(S_Hy, S_Vy, K_y) 463 | 464 | # 47 465 | alpha_r = alpha_star + S_Hf 466 | 467 | # Residual Moment Calculation 468 | M_zr = self.Core.calculate_M_zr(B_r, C_r, D_r, alpha_r, alpha_star) 469 | 470 | fy_pure = state['FY'] # This requires that FY have been calculated already 471 | 472 | mz_pure = -t * fy_pure + M_zr 473 | 474 | return mz_pure 475 | 476 | def calculate_mx(self, state): 477 | 478 | p = self.Coefficients 479 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 480 | 481 | # 86 482 | M_x = p['UNLOADED_RADIUS'] * state['FZ'] * (p['QSX1'] * p['LVMX'] - p['QSX2'] * gamma_star + p['QSX3'] * state['FY'] / p['FNOMIN']) * p['LMX'] 483 | 484 | return M_x 485 | 486 | def calculate_my(self, state): 487 | 488 | p = self.Coefficients 489 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 490 | 491 | # 87 492 | M_y = p['UNLOADED_RADIUS'] * state['FZ'] * (p['QSY1'] + p['QSY2'] * state['FX']/p['FNOMIN'] + p['QSY3'] * abs(state['V'] / p['LONGVL']) + p['QSY4'] * (state['V'] / p['LONGVL'])**4) * p['LMY'] 493 | 494 | return M_y 495 | 496 | def calculate_radius(self, state): 497 | 498 | p = self.Coefficients 499 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 500 | 501 | # If we don't have omega, we use an approximation 502 | omega = state['V'] / (p['UNLOADED_RADIUS'] * 0.98) 503 | 504 | # First we solve for dynamic displacement 505 | # External Effects 506 | speed_effect = p['QV2'] * abs(omega) * p['UNLOADED_RADIUS'] / p['LONGVL'] 507 | fx_effect = (p['QFCX'] * state['FX'] / p['FNOMIN'])**2 508 | fy_effect = (p['QFCY'] * state['FY'] / p['FNOMIN'])**2 509 | camber_effect = p['QFCG'] * gamma_star**2 510 | external_effects = 1.0 + speed_effect - fx_effect - fy_effect + camber_effect 511 | 512 | # Fz/(external_effects * Fz0) = a*x2 + b*x 513 | # 0 = a*x2 + b*x + c 514 | 515 | a = (p['QFZ2'] / p['UNLOADED_RADIUS'])**2 516 | b = p['QFZ1'] / p['UNLOADED_RADIUS'] 517 | c = -(state['FZ'] / (external_effects * p['FNOMIN'])) 518 | 519 | if b**2 - 4*a*c > 0: 520 | rho = (-b + math.sqrt(b**2 - 4*a*c)) / (2 * a) 521 | else: 522 | rho = 999999 523 | 524 | # Then we calculate free-spinning radius 525 | R_omega = p['UNLOADED_RADIUS'] + p['QV1'] * p['UNLOADED_RADIUS'] * (omega * p['UNLOADED_RADIUS'] / p['LONGVL'])**2 526 | 527 | # The loaded radius is the free-spinning radius minus the deflection 528 | R_l = R_omega - rho 529 | 530 | # Effective Rolling Radius 531 | 532 | # Nominal stiffness 533 | C_z0 = p['FNOMIN'] / p['UNLOADED_RADIUS'] * math.sqrt(p['QFZ1']**2 + 4.0 * p['QFZ2']) 534 | if C_z0 == 0.0: 535 | return 0.0, 0.0 536 | 537 | # Eff. Roll. Radius #This is a newer version 538 | R_e_old = R_omega - (p['FNOMIN'] / C_z0) * (p['DREFF'] * math.atan(p['BREFF'] * state['FZ'] / p['FNOMIN']) + p['FREFF'] * state['FZ'] / p['FNOMIN']) 539 | 540 | 541 | # Eff. Roll. Radius Pac 2002 542 | C_z = p['QFZ1'] * p['FNOMIN'] / p['UNLOADED_RADIUS'] 543 | rho_Fz0 = p['FNOMIN'] / (C_z0 * p['LCZ']) 544 | rho_d = rho/rho_Fz0 545 | 546 | R_e = R_omega - rho_Fz0 * (p['DREFF'] * math.atan(p['BREFF'] * rho_d) + p['FREFF'] * rho_d) 547 | 548 | return R_l, R_e 549 | 550 | def calculate_lateral_relaxation_length(self, state): 551 | 552 | 553 | p = self.Coefficients 554 | 555 | if p['PTY2'] == 0: 556 | return 0 557 | 558 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 559 | gamma_y = self.Core.calculate_gamma_y(p, gamma_star) 560 | 561 | # 93 562 | sigma_alpha = p['PTY1'] * math.sin(2.0 * math.atan(state['FZ'] / (p['PTY2'] * p['FNOMIN'] * p['LFZ0']))) * (1 - p['PKY3'] * abs(gamma_y)) * p['UNLOADED_RADIUS'] * p['LFZ0'] * p['LSGAL'] 563 | 564 | return sigma_alpha 565 | 566 | def calculate_longitudinal_relaxation_length(self, state): 567 | 568 | p = self.Coefficients 569 | dfz, alpha_star, gamma_star, kappa = self.calculate_common_values(state) 570 | 571 | # 92 572 | sigma_kappa = state['FZ'] * (p['PTX1'] + p['PTX2'] * dfz) * math.exp(-p['PTX3'] * dfz) * (p['UNLOADED_RADIUS'] / p['FNOMIN']) * p['LSGKP'] 573 | 574 | return sigma_kappa 575 | -------------------------------------------------------------------------------- /code/opentire/TireModel/PAC2002/PAC2002_Core.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | import math 4 | import numpy as np 5 | 6 | # TODO: Use underscore to make it indicate "private" methods 7 | 8 | class PAC2002_Core(): 9 | 10 | #Region "Pure Fy" 11 | def calculate_gamma_y(self, p, gamma_star): 12 | 13 | # 32 14 | gamma_y = gamma_star * p['LGAY'] # Lambda Gamma Y 15 | 16 | return gamma_y 17 | 18 | def calculate_B_y(self, C_y, D_y, K_y): 19 | # Note: This method could call C_y, D_y and K_y internally, but for clarity they are sent as arguments 20 | 21 | # 39 22 | # We need to avoid division by zero 23 | if C_y * D_y == 0.0: 24 | #print 'Division by zero detected in B_Y calculation' 25 | B_y = K_y / 0.000000001 26 | else: 27 | B_y = K_y / (C_y * D_y) # This could cause a divide by zero issue 28 | 29 | return B_y 30 | 31 | def calculate_C_y(self, p): 32 | 33 | # 33 34 | C_y = p['PCY1'] * p['LCY'] 35 | 36 | return C_y 37 | 38 | def calculate_D_y(self, p, state, dfz, gamma_y, zeta1): 39 | 40 | # 34 41 | mu_y = self.calculate_mu_y(p, dfz, gamma_y) 42 | D_y = mu_y * state['FZ'] * zeta1 43 | 44 | return D_y 45 | 46 | def calculate_E_y(self, p, dfz, gamma_y, alpha_y): 47 | 48 | 49 | # 36 50 | # TODO: Sign function difference between Python and equations? 51 | # Note: First virsion had math.copysign, but it acts as an abs(), causing issues. Switched this to np.sign() 52 | E_y = (p['PEY1'] + p['PEY2'] * dfz) * (1 - (p['PEY3'] + p['PEY4'] * gamma_y)*np.sign(alpha_y)) * p['LEY'] 53 | if E_y > 1.0: 54 | E_y = 1.0 55 | 56 | return E_y 57 | 58 | def calculate_K_y(self, p, state, gamma_y, zeta3): 59 | 60 | # 37 61 | denom = (p['PKY2'] * p['FNOMIN'] * p['LFZ0']) 62 | if denom == 0.0: 63 | #print 'Division by zero detected in K_Y calculation' 64 | denom = 0.000000001 65 | 66 | K_y0 = p['PKY1'] * p['FNOMIN'] * math.sin(2.0 * math.atan(state['FZ'] / denom)) * p['LFZ0'] * p['LKY'] 67 | 68 | # 38 69 | K_y = K_y0 * (1-p['PKY3'] * abs(gamma_y)) * zeta3 70 | 71 | return K_y 72 | 73 | def calculate_mu_y(self, p, dfz, gamma_y): 74 | 75 | # 35 76 | mu_y = (p['PDY1'] + p['PDY2'] * dfz) * (1 - p['PDY3'] * gamma_y * gamma_y) * p['LMUY'] # Performance is better with gamma_y * gamma_y instead of gamma_y**2 77 | 78 | return mu_y 79 | 80 | def calculate_S_Hy(self, p, dfz, gamma_y, zeta0, zeta4): 81 | 82 | # 40 83 | S_Hy = (p['PHY1'] + p['PHY2'] * dfz) * p['LHY'] + p['PHY3'] * gamma_y * zeta0 + zeta4 - 1 84 | 85 | return S_Hy 86 | 87 | def calculate_S_Vy(self, p, state, dfz, gamma_y, zeta4): 88 | 89 | # 41 - Moved parenthesis to sit "behind" gamma_y 90 | S_Vy = state['FZ'] * ((p['PVY1'] + p['PVY2'] * dfz) * p['LVY'] + (p['PVY3'] + p['PVY4'] * dfz) * gamma_y) * p['LMUY'] * zeta4 91 | 92 | return S_Vy 93 | 94 | #Region "Combined Fy" 95 | def calculate_B_yk(self, p, alpha): 96 | 97 | # 70 98 | B_yk = p['RBY1'] * math.cos(math.atan(p['RBY2'] * (alpha - p['RBY3']))) * p['LYKA'] 99 | 100 | return B_yk 101 | 102 | def calculate_C_yk(self, p): 103 | 104 | # 71 105 | C_yk = p['RCY1'] 106 | 107 | return C_yk 108 | 109 | def calculate_E_yk(self, p, dfz): 110 | 111 | # 73 112 | E_yk = p['REY1'] + p['REY2'] * dfz 113 | if E_yk > 1.0: 114 | E_yk = 1.0 115 | return E_yk 116 | 117 | def calculate_S_Hyk(self, p, dfz): 118 | 119 | # 74 120 | S_Hyk = p['RHY1'] + p['RHY2'] * dfz 121 | 122 | return S_Hyk 123 | 124 | def calculate_D_Vyk(self, p, state, dfz, gamma_y, alpha, gamma): 125 | 126 | #TODO: Should this be here, or should we pass mu_y instead? What's cleaner? 127 | mu_y = self.calculate_mu_y(p, dfz, gamma_y) 128 | 129 | # 76 130 | D_Vyk = mu_y * state['FZ'] * (p['RVY1'] + p['RVY2'] * dfz + p['RVY3'] * gamma) * math.cos(math.atan(p['RVY4'] * alpha)) 131 | 132 | return D_Vyk 133 | 134 | def calculate_S_Vyk(self, p, kappa, D_Vyk): 135 | 136 | # 75 137 | S_Vyk = D_Vyk * math.sin(p['RVY5'] * math.atan(p['RVY6'] * kappa)) * p['LVYKA'] 138 | 139 | return S_Vyk 140 | 141 | #Region "Pure Fx" 142 | 143 | def calculate_B_x(self, C_x, D_x, K_x): 144 | 145 | # 26 146 | if (C_x * D_x) == 0.0: 147 | B_x = 0.0 148 | 149 | else: 150 | B_x = K_x / (C_x * D_x) 151 | 152 | return B_x 153 | 154 | def calculate_C_x(self, p): 155 | 156 | # 21 157 | C_x = p['PCX1'] * p['LCX'] 158 | 159 | return C_x 160 | 161 | def calculate_D_x(self, p, state, dfz, gamma_star, zeta1): 162 | 163 | mu_x = self.calculate_mu_x(p, dfz, gamma_star) 164 | 165 | # 22 166 | D_x = mu_x * state['FZ'] * zeta1 167 | 168 | return D_x 169 | 170 | def calculate_E_x(self, p, dfz, kappa_x): 171 | 172 | # 24 173 | E_x = (p['PEX1'] + p['PEX2'] * dfz + p['PEX3'] * dfz * dfz) * (1.0 - p['PEX4'] * np.sign(kappa_x)) * p['LEX'] 174 | if E_x > 1.0: 175 | E_x = 1.0 176 | 177 | return E_x 178 | 179 | def calculate_K_x(self, p, state, dfz): 180 | 181 | # 25 182 | K_x = state['FZ'] * (p['PKX1'] + p['PKX2'] * dfz) * math.exp(p['PKX3'] * dfz) * p['LKX'] 183 | # TODO: Check the "exp" function 184 | 185 | return K_x 186 | 187 | def calculate_mu_x(self, p, dfz, gamma_star): 188 | 189 | # 20 190 | gamma_x = gamma_star * p['LGAX'] 191 | 192 | # 23 193 | mu_x = (p['PDX1'] + p['PDX2'] * dfz) * (1.0 - p['PDX3'] * gamma_x ** 2.0) * p['LMUX'] # Note that it is using gamma_x here 194 | 195 | return mu_x 196 | 197 | def calculate_S_Hx(self, p, dfz): 198 | 199 | # 27 200 | S_Hx = (p['PHX1'] + p['PHX2'] * dfz) * p['LHX'] 201 | 202 | return S_Hx 203 | 204 | def calculate_S_Vx(self, p, state, dfz, zeta1): 205 | 206 | # 28 207 | S_Vx = state['FZ'] * (p['PVX1'] + p['PVX2'] * dfz) * p['LVX'] * p['LMUX'] * zeta1 208 | 209 | return S_Vx 210 | 211 | #Region "Combined Fx" 212 | def calculate_S_Hxa(self, p): 213 | 214 | # 65 215 | S_Hxa = p['RHX1'] 216 | 217 | return S_Hxa 218 | 219 | def calculate_B_xa(self, p, kappa): 220 | 221 | # 61 222 | B_xa = p['RBX1'] * math.cos(math.atan(p['RBX2'] * kappa)) * p['LXAL'] 223 | 224 | return B_xa 225 | 226 | def calculate_C_xa(self, p): 227 | 228 | # 62 229 | C_xa = p['RCX1'] 230 | 231 | return C_xa 232 | 233 | def calculate_E_xa(self, p, dfz): 234 | 235 | # 64 236 | E_xa = p['REX1'] + p['REX2'] * dfz 237 | if E_xa > 1.0: 238 | E_xa = 1.0 239 | 240 | return E_xa 241 | 242 | 243 | 244 | #Region "Pure Mz" 245 | 246 | def calculate_gamma_z(self, p, gamma_star): 247 | 248 | # 49 249 | gamma_z = gamma_star * p['LGAZ'] 250 | 251 | return gamma_z 252 | 253 | #Trail Calcs 254 | 255 | def calculate_B_t(self, p, dfz, gamma_z): 256 | 257 | # 50 258 | B_t = (p['QBZ1'] + p['QBZ2'] * dfz + p['QBZ3'] * dfz ** 2) * (1 + p['QBZ4'] * gamma_z + p['QBZ5'] * abs(gamma_z)) * p['LKY'] / p['LMUY'] 259 | 260 | return B_t 261 | 262 | def calculate_C_t(self, p): 263 | 264 | # 51 265 | C_t = p['QCZ1'] 266 | 267 | return C_t 268 | 269 | def calculate_D_t(self,p, state, dfz, gamma_z, zeta5): 270 | 271 | # 52 272 | D_t = state['FZ'] * (p['QDZ1'] + p['QDZ2'] * dfz) * (1 + p['QDZ3'] * gamma_z + p['QDZ4'] * gamma_z ** 2) * p['UNLOADED_RADIUS'] / p['FNOMIN'] * p['LTR'] * zeta5 273 | 274 | return D_t 275 | 276 | def calculate_E_t(self, p, dfz, gamma_z, alpha_t, B_t, C_t): 277 | 278 | # 53 279 | E_t = (p['QEZ1'] + p['QEZ2'] * dfz + p['QEZ3'] * dfz ** 2) * (1 + (p['QEZ4'] + p['QEZ5'] * gamma_z) * ((2/math.pi) * math.atan(B_t * C_t * alpha_t))) 280 | if E_t > 1.0: 281 | E_t = 1.0 282 | 283 | return E_t 284 | 285 | def calculate_S_Ht(self, p, dfz, gamma_z): 286 | 287 | # 54 288 | S_Ht = p['QHZ1'] + p['QHZ2'] * dfz + (p['QHZ3'] + p['QHZ4'] * dfz) * gamma_z 289 | 290 | return S_Ht 291 | 292 | def calculate_t(self,p, B_t, C_t, D_t, E_t, alpha_t, alpha_star): 293 | 294 | # 44 - Trail 295 | t = D_t * math.cos(C_t * math.atan(B_t * alpha_t - E_t * (B_t * alpha_t - math.atan(B_t * alpha_t)))) * math.cos(alpha_star) 296 | 297 | return t 298 | 299 | #Residual Moment Calcs 300 | 301 | def calculate_B_r(self, p, B_y, C_y, zeta6): 302 | 303 | # 55 304 | B_r = (p['QBZ9'] * p['LKY'] / p['LMUY'] + p['QBZ10'] * B_y * C_y) * zeta6 305 | 306 | return B_r 307 | 308 | def calculate_C_r(self, zeta7): 309 | 310 | # after 55 311 | C_r = zeta7 312 | 313 | return C_r 314 | 315 | def calculate_D_r(self, p, state, dfz, gamma_z, zeta8): 316 | 317 | # 56 318 | D_r = state['FZ'] * ((p['QDZ6'] + p['QDZ7'] * dfz) * p['LRES'] + (p['QDZ8'] + p['QDZ9'] * dfz) * gamma_z) * p['UNLOADED_RADIUS'] * p['LMUY'] + zeta8 - 1.0 319 | 320 | return D_r 321 | 322 | def calculate_S_Hf(self, S_Hy, S_Vy, K_y): 323 | 324 | # 48 325 | S_Hf = S_Hy + S_Vy / K_y 326 | 327 | return S_Hf 328 | 329 | def calculate_M_zr(self, B_r, C_r, D_r, alpha_r, alpha_star ): 330 | 331 | # 46 - Residual Moment 332 | M_zr = D_r * math.cos(C_r * math.atan(B_r * alpha_r)) * math.cos(alpha_star) 333 | 334 | return M_zr 335 | -------------------------------------------------------------------------------- /code/opentire/TireModel/PAC2002/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | from opentire.TireModel.PAC2002.PAC2002 import PAC2002 4 | -------------------------------------------------------------------------------- /code/opentire/TireModel/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | from opentire.TireModel.PAC2002 import PAC2002 4 | from opentire.TireModel.tiremodelbase import TireModelBase 5 | from opentire.TireModel.solvermode import SolverMode 6 | -------------------------------------------------------------------------------- /code/opentire/TireModel/solvermode.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | 4 | class SolverMode(): 5 | All = 0 6 | Fy = 1 7 | Fx = 2 8 | Mz = 3 9 | Mx = 4 10 | Radius = 5 11 | Relaxation = 6 12 | PureFy = 7 13 | PureFx = 8 14 | PureMx = 9 15 | PureMz = 10 16 | -------------------------------------------------------------------------------- /code/opentire/TireModel/tiremodelbase.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | import abc 3 | 4 | class TireModelBase(object): 5 | __metaclass__ = abc.ABCMeta 6 | 7 | Coefficients = None 8 | ModelInfo = None 9 | 10 | @abc.abstractmethod 11 | def getmodelinfo(self): 12 | """Return information about the model""" 13 | return 14 | 15 | @abc.abstractmethod 16 | def createmodel(self): 17 | """Create a default model""" 18 | 19 | @abc.abstractmethod 20 | def load(self, fname): 21 | """Retrieve data from the input source and return an object.""" 22 | return 23 | 24 | @abc.abstractmethod 25 | def save(self, fname, data): 26 | """Save the data object to the output.""" 27 | return 28 | 29 | @abc.abstractmethod 30 | def solve(self, state, mode=0): 31 | """Calculate steady state force""" 32 | return 33 | 34 | @abc.abstractmethod 35 | def getparameters(self): 36 | """Return the parameters dictionary""" 37 | return 38 | 39 | @abc.abstractmethod 40 | def setparameters(self, params): 41 | """Set the parameters""" 42 | -------------------------------------------------------------------------------- /code/opentire/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | from opentire.opentire import OpenTire 4 | -------------------------------------------------------------------------------- /code/opentire/opentire.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | 3 | from opentire.TireModel import * 4 | 5 | 6 | class OpenTire: 7 | 8 | ModelTypes = dict() 9 | 10 | def createmodel(self, modelname): 11 | 12 | tm = None 13 | 14 | if modelname == 'Harty': 15 | tm = None #Change to address print functionaility issues in Py2/Py3 16 | 17 | elif modelname == 'PAC2002': 18 | tm = PAC2002() 19 | 20 | else: 21 | return None # Of we cam 22 | 23 | tm.createmodel() 24 | return tm 25 | 26 | def __init__(self): 27 | self.ModelTypes = \ 28 | {'PAC2002': 'Pacejka 2002', 29 | 'Fiala': 'Fiala Tire Model Implementation'} 30 | 31 | def getmodellist(self): 32 | return self.ModelTypes 33 | -------------------------------------------------------------------------------- /code/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='OpenTire', 5 | version='0.1', 6 | description='An open-source mathematical tire model libraries for tire and vehicle research and development.', 7 | url='https://github.com/OpenTire/OpenTire', 8 | author='OpenTire', 9 | license='MIT', 10 | packages=find_packages(), 11 | zip_safe=False, 12 | install_requires=['numpy'], 13 | classifiers=[ 14 | 'Programming Language :: Python :: 2' 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /code/testEnv.py: -------------------------------------------------------------------------------- 1 | __author__ = 'henningo' 2 | import pylab as plt 3 | 4 | import numpy as np 5 | 6 | from opentire import OpenTire 7 | from opentire.Core import TireState 8 | from opentire.Core import TIRFile 9 | 10 | OpenTire = OpenTire() 11 | 12 | myTiremodel = OpenTire.createmodel('PAC2002') 13 | 14 | ##Or you can load in a tir file 15 | #tir_file = TIRFile() 16 | #fname = 'C:/myfile.tir' 17 | #myTiremodel = tir_file.load(fname) 18 | 19 | state = TireState() 20 | 21 | state['FZ'] = 1500 22 | state['IA'] = 0.0 23 | state['SR'] = 0.0 24 | state['SA'] = 0.0 25 | state['FY'] = 0.0 26 | state['V'] = 10.0 27 | state['P'] = 260000 28 | 29 | 30 | xchan = 'SA' 31 | ychan = 'FY' 32 | 33 | loads = [500, 1000, 1500] 34 | 35 | slip_angles = np.arange(-12, 12, 0.1) * 0.01 36 | 37 | 38 | i = 1 39 | 40 | for fz in loads: 41 | 42 | yvals = [] 43 | 44 | for in slip_angles: 45 | state[xchan] = float(sa_val) 46 | state['FZ'] = float(fz) 47 | resultstate = myTiremodel.solve(state) 48 | sa_val = resultstate[xchan] 49 | yval = resultstate[ychan] 50 | 51 | yvals = np.append(yvals, yval) 52 | 53 | plt.plot(xvals*180/3.14, yvals, label=fz) 54 | 55 | plt.show() 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/FY_SA_Example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "deletable": true, 7 | "editable": true 8 | }, 9 | "source": [ 10 | "Getting Started with OpenTire w/ Jupyter Notebook\n", 11 | "============================\n", 12 | "Generate a lateral force vs slip angle plot" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": { 18 | "deletable": true, 19 | "editable": true 20 | }, 21 | "source": [ 22 | "Import OpenTire and other libraries used in this demonstration" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": { 29 | "collapsed": true, 30 | "deletable": true, 31 | "editable": true 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "from opentire import OpenTire\n", 36 | "from opentire.Core import TireState\n", 37 | "from opentire.Core import TIRFile\n", 38 | "\n", 39 | "from pprint import pprint\n", 40 | "import numpy as np\n", 41 | "import matplotlib.pyplot as plt" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": { 47 | "deletable": true, 48 | "editable": true 49 | }, 50 | "source": [ 51 | "Initialize the OpenTire factory and create a Pacejka 2002 tire model" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": { 58 | "collapsed": true, 59 | "deletable": true, 60 | "editable": true 61 | }, 62 | "outputs": [], 63 | "source": [ 64 | "openTire = OpenTire()\n", 65 | "myTireModel = openTire.createmodel('PAC2002')" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": { 71 | "deletable": true, 72 | "editable": true 73 | }, 74 | "source": [ 75 | "Initialize the tire state" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": { 82 | "collapsed": true, 83 | "deletable": true, 84 | "editable": true 85 | }, 86 | "outputs": [], 87 | "source": [ 88 | "state = TireState()\n", 89 | "\n", 90 | "state['FZ'] = 1500\n", 91 | "state['IA'] = 0.0\n", 92 | "state['SR'] = 0.0\n", 93 | "state['SA'] = 0.0\n", 94 | "state['FY'] = 0.0\n", 95 | "state['V'] = 10.0\n", 96 | "state['P'] = 260000" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": { 102 | "deletable": true, 103 | "editable": true 104 | }, 105 | "source": [ 106 | "Solving for the tire forces will update the tire state" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": { 113 | "collapsed": false, 114 | "deletable": true, 115 | "editable": true 116 | }, 117 | "outputs": [], 118 | "source": [ 119 | "myTireModel.solve(state)\n", 120 | "pprint(state)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": { 126 | "deletable": true, 127 | "editable": true 128 | }, 129 | "source": [ 130 | "Iterate over vertical loads and slip angles to generate a lateral force vs slip angle plot at three different vertical loads." 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": { 137 | "collapsed": false, 138 | "deletable": true, 139 | "editable": true 140 | }, 141 | "outputs": [], 142 | "source": [ 143 | "# Setup the simulation range\n", 144 | "vertical_loads = [500, 1000, 2000]\n", 145 | "slip_angles = np.arange(-12, 12, 0.1) * 3.14 / 180\n", 146 | "\n", 147 | "# Initialize the lateral force result\n", 148 | "lateral_force = []\n", 149 | "\n", 150 | "for fz in vertical_loads:\n", 151 | " lateral_force = []\n", 152 | " state['FZ'] = fz\n", 153 | " \n", 154 | " for sa in slip_angles:\n", 155 | " # Solving\n", 156 | " state['SA'] = sa\n", 157 | " myTireModel.solve(state)\n", 158 | " lateral_force.append(state['FY'])\n", 159 | "\n", 160 | " # Plot the series\n", 161 | " plt.plot(slip_angles * 180 / 3.14, lateral_force, label=fz)\n", 162 | "\n", 163 | "# Plotting\n", 164 | "plt.grid()\n", 165 | "plt.xlabel('Slip Angle [deg]')\n", 166 | "plt.ylabel('Lateral Force [N]')\n", 167 | "plt.title('Lateral Force vs. Slip Angle')\n", 168 | "plt.show()\n" 169 | ] 170 | } 171 | ], 172 | "metadata": { 173 | "kernelspec": { 174 | "display_name": "Python 2", 175 | "language": "python", 176 | "name": "python2" 177 | }, 178 | "language_info": { 179 | "codemirror_mode": { 180 | "name": "ipython", 181 | "version": 2 182 | }, 183 | "file_extension": ".py", 184 | "mimetype": "text/x-python", 185 | "name": "python", 186 | "nbconvert_exporter": "python", 187 | "pygments_lexer": "ipython2", 188 | "version": "2.7.13" 189 | } 190 | }, 191 | "nbformat": 4, 192 | "nbformat_minor": 2 193 | } 194 | -------------------------------------------------------------------------------- /examples/Moment_Diagram_Example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "deletable": true, 7 | "editable": true 8 | }, 9 | "source": [ 10 | "OpenTire Moment Method Example\n", 11 | "=================\n", 12 | "Draws a constant velocity force-moment diagram of a bicycle using the OpenTire library" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": { 18 | "deletable": true, 19 | "editable": true 20 | }, 21 | "source": [ 22 | "Import OpenTire and other libraries used in this demonstration" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": { 29 | "collapsed": false, 30 | "deletable": true, 31 | "editable": true 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "from opentire import OpenTire\n", 36 | "from opentire.Core import TireState\n", 37 | "\n", 38 | "import numpy as np\n", 39 | "import matplotlib.pyplot as plt" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": { 45 | "deletable": true, 46 | "editable": true 47 | }, 48 | "source": [ 49 | "Define the vehicle class" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": { 56 | "collapsed": false, 57 | "deletable": true, 58 | "editable": true 59 | }, 60 | "outputs": [], 61 | "source": [ 62 | "class Vehicle():\n", 63 | " def __init__(self):\n", 64 | " self._mass = 1000\n", 65 | " self._wb = 1\n", 66 | " self._wd = 0.5\n", 67 | " self._ft = None\n", 68 | " self._rt = None\n", 69 | " \n", 70 | " @property\n", 71 | " def mass(self):\n", 72 | " return self._mass\n", 73 | " \n", 74 | " @mass.setter\n", 75 | " def mass(self, value):\n", 76 | " if value <= 0:\n", 77 | " raise ValueError('Mass must be greater than zero')\n", 78 | " self._mass = value\n", 79 | " \n", 80 | " @property\n", 81 | " def wheelbase(self):\n", 82 | " return self._wb\n", 83 | " \n", 84 | " @wheelbase.setter\n", 85 | " def wheelbase(self, value):\n", 86 | " if value <= 0:\n", 87 | " raise ValueError('Wheelbase must be greater than zero')\n", 88 | " self._wb = value\n", 89 | " \n", 90 | " @property\n", 91 | " def weight_dist(self):\n", 92 | " return self._wd\n", 93 | " \n", 94 | " @weight_dist.setter\n", 95 | " def weight_dist(self, value):\n", 96 | " if value >= 1 or value <= 0:\n", 97 | " raise ValueError('Weight distribution must be a ratio between 0 and 1')\n", 98 | " self._wd = value\n", 99 | " \n", 100 | " @property\n", 101 | " def front_tire(self):\n", 102 | " return self._ft\n", 103 | " \n", 104 | " @front_tire.setter\n", 105 | " def front_tire(self, value):\n", 106 | " if not isinstance(value, TireModelBase) and value is not None:\n", 107 | " raise TypeError('Front tire must be a OpenTire model')\n", 108 | " self._ft = value\n", 109 | " \n", 110 | " @property\n", 111 | " def rear_tire(self):\n", 112 | " return self._rt\n", 113 | " \n", 114 | " @front_tire.setter\n", 115 | " def rear_tire(self, value):\n", 116 | " if not isinstance(value, TireModelBase) and values is not None:\n", 117 | " raise TypeError('Rear tire must be a OpenTire model')\n", 118 | " self._rt = value\n", 119 | " \n", 120 | " @property\n", 121 | " def length_a(self):\n", 122 | " return self.wheelbase * (1 - self.weight_dist)\n", 123 | " \n", 124 | " @property\n", 125 | " def length_b(self):\n", 126 | " return self.wheelbase * self.weight_dist" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": { 132 | "deletable": true, 133 | "editable": true 134 | }, 135 | "source": [ 136 | "Define the Moment Method class" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": { 143 | "collapsed": false, 144 | "deletable": true, 145 | "editable": true 146 | }, 147 | "outputs": [], 148 | "source": [ 149 | "class MomentMethodSolver():\n", 150 | " def __init__(self):\n", 151 | " self._vehicle = None\n", 152 | " self._beta = np.linspace(-12, 12, 25) * 3.14 / 180\n", 153 | " self._delta = np.linspace(-12, 12, 25) * 3.14 / 180\n", 154 | " self._velocity = 36\n", 155 | " \n", 156 | " @property\n", 157 | " def vehicle(self):\n", 158 | " return self._vehicle\n", 159 | " \n", 160 | " @vehicle.setter\n", 161 | " def vehicle(self, value):\n", 162 | " if not isinstance(value, Vehicle) and value is not None:\n", 163 | " raise TypeError('Solver vehicle must be of Vehicle type')\n", 164 | " self._vehicle = value\n", 165 | " \n", 166 | " @property\n", 167 | " def beta_range(self):\n", 168 | " return self._beta\n", 169 | " \n", 170 | " @beta_range.setter\n", 171 | " def beta_range(self, value):\n", 172 | " self._beta = value\n", 173 | " \n", 174 | " @property\n", 175 | " def delta_range(self):\n", 176 | " return self._delta\n", 177 | " \n", 178 | " @delta_range.setter\n", 179 | " def delta_range(self, value):\n", 180 | " self._delta = value \n", 181 | " \n", 182 | " @property\n", 183 | " def velocity(self):\n", 184 | " return self._velocity\n", 185 | " \n", 186 | " @velocity.setter\n", 187 | " def velocity(self, value):\n", 188 | " self._velocity = value\n", 189 | " \n", 190 | " def solve(self):\n", 191 | " fy = np.empty([len(self.beta_range), len(self.delta_range)])\n", 192 | " mz = np.empty([len(self.beta_range), len(self.delta_range)])\n", 193 | " initial_guess = (0, 0)\n", 194 | " \n", 195 | " for i, beta in enumerate(self.beta_range):\n", 196 | " for j, delta in enumerate(self.delta_range):\n", 197 | " # Use previous solution as a guess\n", 198 | " if j > 0: initial_guess = self._invertSolution(fy[i][j-1], mz[i][j-1])\n", 199 | " elif i > 0: initial_guess = self._invertSolution(fy[i-1][j], mz[i-1][j])\n", 200 | " else: initial_guess = (0, 0)\n", 201 | " \n", 202 | " result = self._solve(beta, delta, initial_guess)\n", 203 | " fy[i][j] = result[0]\n", 204 | " mz[i][j] = result[1]\n", 205 | " \n", 206 | " return (fy, mz)\n", 207 | " \n", 208 | " def _solve(self, beta, delta, initial_guess = (0, 0)):\n", 209 | " state = TireState()\n", 210 | " state['FZ'] = 1500\n", 211 | " state['IA'] = 0.0\n", 212 | " state['SR'] = 0.0\n", 213 | " state['SA'] = 0.0\n", 214 | " state['FY'] = 0.0\n", 215 | " state['V'] = 10.0\n", 216 | " state['P'] = 260000\n", 217 | " \n", 218 | " MAX_ITER = 100\n", 219 | " n = 0\n", 220 | " error = 9999\n", 221 | " tolerance = 0.1\n", 222 | " yaw_velocity = 0\n", 223 | " \n", 224 | " front_force = initial_guess[0]\n", 225 | " rear_force = initial_guess[1]\n", 226 | " \n", 227 | " while (n < MAX_ITER and abs(error) > tolerance): \n", 228 | " # Yaw rate\n", 229 | " yaw_velocity = (front_force + rear_force) / (self.vehicle.mass * self.velocity)\n", 230 | " error = front_force + rear_force\n", 231 | " \n", 232 | " # Slip Angles\n", 233 | " sa_front = beta - delta + yaw_velocity * self.vehicle.length_a / self.velocity\n", 234 | " sa_rear = beta - yaw_velocity * self.vehicle.length_b / self.velocity\n", 235 | "\n", 236 | " # Front Tire\n", 237 | " state['SA'] = sa_front\n", 238 | " state['FZ'] = 0.5 * 9.81 * self.vehicle.mass * self.vehicle.weight_dist\n", 239 | " self.vehicle.front_tire.solve(state)\n", 240 | "\n", 241 | " front_force = state['FY']\n", 242 | " state['SA'] = -sa_front\n", 243 | " self.vehicle.front_tire.solve(state)\n", 244 | " front_force -= state['FY']\n", 245 | "\n", 246 | " # Rear Tire\n", 247 | " state['SA'] = sa_rear\n", 248 | " state['FZ'] = 0.5 * 9.81 * self.vehicle.mass * (1 - self.vehicle.weight_dist)\n", 249 | " self.vehicle.rear_tire.solve(state)\n", 250 | "\n", 251 | " rear_force = state['FY']\n", 252 | " state['SA'] = -sa_rear\n", 253 | " self.vehicle.rear_tire.solve(state)\n", 254 | " rear_force -= state['FY']\n", 255 | " \n", 256 | " error -= front_force + rear_force\n", 257 | " n += 1\n", 258 | " \n", 259 | " return (front_force + rear_force,\n", 260 | " front_force * self.vehicle.length_a - rear_force * self.vehicle.length_b)\n", 261 | " \n", 262 | " def _invertSolution(self, lateral_force, yaw_moment):\n", 263 | " front_force = (1 / (self.vehicle.length_a + self.vehicle.length_b)) * (self.vehicle.length_b * lateral_force\n", 264 | " + yaw_moment)\n", 265 | " rear_force = (1 / (self.vehicle.length_a + self.vehicle.length_b)) * (self.vehicle.length_a * lateral_force \n", 266 | " - yaw_moment)\n", 267 | " \n", 268 | " return (front_force, rear_force)" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": { 274 | "deletable": true, 275 | "editable": true 276 | }, 277 | "source": [ 278 | "Run simulation" 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": null, 284 | "metadata": { 285 | "collapsed": false, 286 | "deletable": true, 287 | "editable": true, 288 | "scrolled": true 289 | }, 290 | "outputs": [], 291 | "source": [ 292 | "openTire = OpenTire()\n", 293 | "\n", 294 | "myVehicle = Vehicle()\n", 295 | "myVehicle.mass = 1250 # kg\n", 296 | "myVehicle.wheelbase = 2.4 # m\n", 297 | "myVehicle.weight_dist = 0.47 # ratio\n", 298 | "myVehicle.front_tire = openTire.createmodel('PAC2002')\n", 299 | "myVehicle.rear_tire = openTire.createmodel('PAC2002')\n", 300 | "\n", 301 | "solver = MomentMethodSolver()\n", 302 | "solver.vehicle = myVehicle\n", 303 | "solver.beta = np.linspace(-15, 16, 31) * 3.14 / 180\n", 304 | "solver.delta = np.linspace(-15, 16, 31) * 3.14 / 180\n", 305 | "solver.velocity = 70 / 3.6\n", 306 | "\n", 307 | "force_moments = solver.solve()\n", 308 | "lateral_accel = force_moments[0] / myVehicle.mass / 9.81\n", 309 | "yaw_moment = force_moments[1]\n", 310 | "\n", 311 | "plt.plot(lateral_accel[:][:], yaw_moment[:][:], color='black')\n", 312 | "plt.plot(np.transpose(lateral_accel[:][:]), np.transpose(yaw_moment[:][:]), color='red')\n", 313 | "\n", 314 | "plt.grid()\n", 315 | "plt.xlabel(\"Lateral Acceleration [g]\")\n", 316 | "plt.ylabel(\"Yaw Moment [Nm]\")\n", 317 | "\n", 318 | "plt.show()" 319 | ] 320 | } 321 | ], 322 | "metadata": { 323 | "kernelspec": { 324 | "display_name": "Python 2", 325 | "language": "python", 326 | "name": "python2" 327 | }, 328 | "language_info": { 329 | "codemirror_mode": { 330 | "name": "ipython", 331 | "version": 2 332 | }, 333 | "file_extension": ".py", 334 | "mimetype": "text/x-python", 335 | "name": "python", 336 | "nbconvert_exporter": "python", 337 | "pygments_lexer": "ipython2", 338 | "version": "2.7.13" 339 | } 340 | }, 341 | "nbformat": 4, 342 | "nbformat_minor": 2 343 | } 344 | -------------------------------------------------------------------------------- /examples/SA_Sweep_Example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from opentire import OpenTire 4 | from opentire.Core import TireState 5 | from opentire.Core import TIRFile 6 | 7 | import numpy as np 8 | 9 | if __name__ == "__main__": 10 | 11 | # Initialize the tire model 12 | openTire = OpenTire() 13 | myTireModel = openTire.createmodel('PAC2002') 14 | 15 | # Initialize the tire state 16 | state = TireState() 17 | state['FZ'] = 1500 18 | state['IA'] = 0.0 19 | state['SR'] = 0.0 20 | state['SA'] = 0.0 21 | state['FY'] = 0.0 22 | state['V'] = 10.0 23 | state['P'] = 260000 24 | 25 | # Define the slip angle range 26 | slip_angles = np.arange(-12, 13, 1) * 3.14 / 180 27 | 28 | # Print out some pretty formatting 29 | print('OpenTire Slip Angle Sweep Demo\n') 30 | print('{0:>10} | {1:>10} | {2:>10} | {3:>10} | {4:>10}' 31 | .format('SA [deg]', 32 | 'FZ [N]', 33 | 'FY [N]', 34 | 'MZ [Nm]', 35 | 'MX [Nm]')) 36 | print('=' * 62) 37 | 38 | # Calculate and print out the tire model outputs 39 | for sa in slip_angles: 40 | state['SA'] = sa 41 | myTireModel.solve(state) 42 | print('{0:>10.0f} | {1:>10.0f} | {2:>10.1f} | {3:>10.1f} | {4:>10.1f}' 43 | .format(state['SA'] * 180 / 3.14, 44 | state['FZ'], 45 | state['FY'], 46 | state['MZ'], 47 | state['MX'])) 48 | 49 | --------------------------------------------------------------------------------