OMCompiler/SimulationRuntime/c/optimization/optimizer_main.c
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file belongs to the OpenModelica Run-Time System | ||
| 3 | * | ||
| 4 | * Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC), c/o Linköpings | ||
| 5 | * universitet, Department of Computer and Information Science, SE-58183 Linköping, Sweden. All rights | ||
| 6 | * reserved. | ||
| 7 | * | ||
| 8 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF THE BSD NEW LICENSE OR THE | ||
| 9 | * AGPL VERSION 3 LICENSE OR THE OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. ANY | ||
| 10 | * USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES RECIPIENT'S | ||
| 11 | * ACCEPTANCE OF THE BSD NEW LICENSE OR THE OSMC PUBLIC LICENSE OR THE AGPL | ||
| 12 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. | ||
| 13 | * | ||
| 14 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) Public License | ||
| 15 | * (OSMC-PL) are obtained from OSMC, either from the above address, from the URLs: | ||
| 16 | * http://www.openmodelica.org or https://github.com/OpenModelica/ or | ||
| 17 | * http://www.ida.liu.se/projects/OpenModelica, and in the OpenModelica distribution. GNU | ||
| 18 | * AGPL version 3 is obtained from: https://www.gnu.org/licenses/licenses.html#GPL. The BSD NEW | ||
| 19 | * License is obtained from: http://www.opensource.org/licenses/BSD-3-Clause. | ||
| 20 | * | ||
| 21 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 22 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY | ||
| 23 | * SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF | ||
| 24 | * OSMC-PL. | ||
| 25 | * | ||
| 26 | */ | ||
| 27 | |||
| 28 | /*! optimizer_main.c | ||
| 29 | * move model data in optimizer structure | ||
| 30 | */ | ||
| 31 | |||
| 32 | #include "OptimizerData.h" | ||
| 33 | #include "OptimizerLocalFunction.h" | ||
| 34 | #include "simulation_data.h" | ||
| 35 | #include "simulation/options.h" | ||
| 36 | |||
| 37 | static inline int optimizationWithIpopt(OptData*optData); | ||
| 38 | static inline void freeOptimizerData(OptData*optData); | ||
| 39 | static const char* firstArrayVariable(const MODEL_DATA *modelData); | ||
| 40 | |||
| 41 | ✗ | int runOptimizer(DATA* data, threadData_t *threadData, SOLVER_INFO* solverInfo){ | |
| 42 | OptData *optData, optData_; | ||
| 43 | const char *arrayVariable; | ||
| 44 | |||
| 45 | /* The optimizer maps variables to optimization variables by scalar index. */ | ||
| 46 | ✗ | arrayVariable = firstArrayVariable(data->modelData); | |
| 47 | ✗ | if (arrayVariable != NULL) { | |
| 48 | ✗ | throwStreamPrint(threadData, "Optimization does not support array variables, but %s is an array. " | |
| 49 | "Use --simCodeScalarize=true.", arrayVariable); | ||
| 50 | } | ||
| 51 | |||
| 52 | ✗ | solverInfo->solverData = &optData_; | |
| 53 | ✗ | data->simulationInfo->noThrowDivZero = 1; | |
| 54 | |||
| 55 | ✗ | pickUpModelData(data, threadData, solverInfo); | |
| 56 | ✗ | optData = (OptData*) solverInfo->solverData; | |
| 57 | |||
| 58 | ✗ | initial_guess_optimizer(optData, solverInfo); | |
| 59 | ✗ | allocate_der_struct(&optData->s, &optData->dim ,data, optData); | |
| 60 | |||
| 61 | ✗ | const int res = optimizationWithIpopt(optData); | |
| 62 | ✗ | res2file(optData, solverInfo, optData->ipop.vopt); | |
| 63 | ✗ | freeOptimizerData(optData); | |
| 64 | ✗ | if(res == 0 /*Solve_Succeeded*/ || res == 1 /*Solved_To_Acceptable_Level*/) | |
| 65 | ✗ | return 0; | |
| 66 | return -1; | ||
| 67 | } | ||
| 68 | |||
| 69 | /** | ||
| 70 | * @brief Find first array variable or parameter. | ||
| 71 | * | ||
| 72 | * @param modelData Model data. | ||
| 73 | * @return const char* Name of first array variable or parameter, | ||
| 74 | * NULL if all variables are scalars. | ||
| 75 | */ | ||
| 76 | ✗ | static const char* firstArrayVariable(const MODEL_DATA *modelData) | |
| 77 | { | ||
| 78 | long i; | ||
| 79 | |||
| 80 | #define FIND_ARRAY(VARS, N) \ | ||
| 81 | for (i = 0; i < (N); ++i) { \ | ||
| 82 | if ((VARS)[i].dimension.numberOfDimensions > 0) { \ | ||
| 83 | return (VARS)[i].info.name; \ | ||
| 84 | } \ | ||
| 85 | } | ||
| 86 | |||
| 87 | ✗ | FIND_ARRAY(modelData->realVarsData, modelData->nVariablesRealArray) | |
| 88 | ✗ | FIND_ARRAY(modelData->integerVarsData, modelData->nVariablesIntegerArray) | |
| 89 | ✗ | FIND_ARRAY(modelData->booleanVarsData, modelData->nVariablesBooleanArray) | |
| 90 | ✗ | FIND_ARRAY(modelData->stringVarsData, modelData->nVariablesStringArray) | |
| 91 | ✗ | FIND_ARRAY(modelData->realParameterData, modelData->nParametersRealArray) | |
| 92 | ✗ | FIND_ARRAY(modelData->integerParameterData, modelData->nParametersIntegerArray) | |
| 93 | ✗ | FIND_ARRAY(modelData->booleanParameterData, modelData->nParametersBooleanArray) | |
| 94 | ✗ | FIND_ARRAY(modelData->stringParameterData, modelData->nParametersStringArray) | |
| 95 | |||
| 96 | #undef FIND_ARRAY | ||
| 97 | |||
| 98 | return NULL; | ||
| 99 | } | ||
| 100 | |||
| 101 | /*! | ||
| 102 | * run optimization with ipopt | ||
| 103 | * author: Vitalij Ruge | ||
| 104 | **/ | ||
| 105 | ✗ | static inline int optimizationWithIpopt(OptData*optData){ | |
| 106 | IpoptProblem nlp = NULL; | ||
| 107 | |||
| 108 | ✗ | const int NV = optData->dim.NV; | |
| 109 | ✗ | const int NRes = optData->dim.NRes; | |
| 110 | ✗ | const int nsi = optData->dim.nsi; | |
| 111 | ✗ | const int np = optData->dim.np; | |
| 112 | ✗ | const int nx = optData->dim.nx; | |
| 113 | ✗ | const int NJ = optData->dim.nJderx; | |
| 114 | ✗ | const int NJf = optData->dim.nJfderx; | |
| 115 | ✗ | const int nH0 = optData->dim.nH0_; | |
| 116 | ✗ | const int nH1 = optData->dim.nH1_; | |
| 117 | ✗ | const int njac = np*(NJ*nsi + nx*(np*nsi - 1)) + NJf; | |
| 118 | ✗ | const int nhess = (nsi*np-1)*nH0+nH1; | |
| 119 | |||
| 120 | ✗ | ipnumber * Vmin = optData->bounds.Vmin; | |
| 121 | ✗ | ipnumber * Vmax = optData->bounds.Vmax; | |
| 122 | ✗ | ipnumber * gmin = optData->ipop.gmin; | |
| 123 | ✗ | ipnumber * gmax = optData->ipop.gmax; | |
| 124 | ✗ | ipnumber * vopt = optData->ipop.vopt; | |
| 125 | ✗ | ipnumber * mult_g = optData->ipop.mult_g; | |
| 126 | ✗ | ipnumber * mult_x_L = optData->ipop.mult_x_L; | |
| 127 | ✗ | ipnumber * mult_x_U = optData->ipop.mult_x_U; | |
| 128 | ipnumber obj; | ||
| 129 | |||
| 130 | char *cflags; | ||
| 131 | int max_iter = 5000; | ||
| 132 | int res = 0; | ||
| 133 | |||
| 134 | ✗ | nlp = CreateIpoptProblem(NV, Vmin, Vmax, | |
| 135 | NRes, gmin, gmax, njac, nhess, 0, &evalfF, | ||
| 136 | &evalfG, &evalfDiffF, &evalfDiffG, &ipopt_h); | ||
| 137 | |||
| 138 | /********************************************************************/ | ||
| 139 | /******************* ipopt flags ************************/ | ||
| 140 | /********************************************************************/ | ||
| 141 | |||
| 142 | /*tol */ | ||
| 143 | ✗ | AddIpoptNumOption(nlp, "tol", optData->data->simulationInfo->tolerance); | |
| 144 | ✗ | AddIpoptStrOption(nlp, "evaluate_orig_obj_at_resto_trial", "yes"); | |
| 145 | |||
| 146 | /* print level */ | ||
| 147 | ✗ | if(OMC_ACTIVE_STREAM(OMC_LOG_IPOPT_FULL)){ | |
| 148 | ✗ | AddIpoptIntOption(nlp, "print_level", 7); | |
| 149 | ✗ | }else if(OMC_ACTIVE_STREAM(OMC_LOG_IPOPT)){ | |
| 150 | ✗ | AddIpoptIntOption(nlp, "print_level", 5); | |
| 151 | ✗ | }else if(OMC_ACTIVE_STREAM(OMC_LOG_STATS)){ | |
| 152 | ✗ | AddIpoptIntOption(nlp, "print_level", 3); | |
| 153 | }else { | ||
| 154 | ✗ | AddIpoptIntOption(nlp, "print_level", 2); | |
| 155 | } | ||
| 156 | ✗ | AddIpoptIntOption(nlp, "file_print_level", 0); | |
| 157 | |||
| 158 | /* derivative_test */ | ||
| 159 | ✗ | if(OMC_ACTIVE_STREAM(OMC_LOG_IPOPT_JAC) && OMC_ACTIVE_STREAM(OMC_LOG_IPOPT_HESSE)){ | |
| 160 | ✗ | AddIpoptIntOption(nlp, "print_level", 4); | |
| 161 | ✗ | AddIpoptStrOption(nlp, "derivative_test", "second-order"); | |
| 162 | ✗ | }else if(OMC_ACTIVE_STREAM(OMC_LOG_IPOPT_JAC)){ | |
| 163 | ✗ | AddIpoptIntOption(nlp, "print_level", 4); | |
| 164 | ✗ | AddIpoptStrOption(nlp, "derivative_test", "first-order"); | |
| 165 | ✗ | }else if(OMC_ACTIVE_STREAM(OMC_LOG_IPOPT_HESSE)){ | |
| 166 | ✗ | AddIpoptIntOption(nlp, "print_level", 4); | |
| 167 | ✗ | AddIpoptStrOption(nlp, "derivative_test", "only-second-order"); | |
| 168 | }else{ | ||
| 169 | ✗ | AddIpoptStrOption(nlp, "derivative_test", "none"); | |
| 170 | } | ||
| 171 | |||
| 172 | |||
| 173 | ✗ | cflags = (char*)omc_flagValue[FLAG_IPOPT_HESSE]; | |
| 174 | ✗ | if(cflags){ | |
| 175 | ✗ | if(!strcmp(cflags,"BFGS")) | |
| 176 | ✗ | AddIpoptStrOption(nlp, "hessian_approximation", "limited-memory"); | |
| 177 | ✗ | else if(!strcmp(cflags,"const") || !strcmp(cflags,"CONST")) | |
| 178 | ✗ | AddIpoptStrOption(nlp, "hessian_constant", "yes"); | |
| 179 | ✗ | else if(!(!strcmp(cflags,"num") || !strcmp(cflags,"NUM"))) | |
| 180 | ✗ | warningStreamPrint(OMC_LOG_STDOUT, 0, "not support ipopt_hesse=%s",cflags); | |
| 181 | } | ||
| 182 | |||
| 183 | /*linear_solver e.g. mumps, MA27, MA57,... | ||
| 184 | * be sure HSL solver are installed if your try HSL solver*/ | ||
| 185 | ✗ | cflags = (char*)omc_flagValue[FLAG_LS_IPOPT]; | |
| 186 | ✗ | if(cflags) | |
| 187 | ✗ | AddIpoptStrOption(nlp, "linear_solver", cflags); | |
| 188 | ✗ | AddIpoptNumOption(nlp,"mumps_pivtolmax",1e-5); | |
| 189 | |||
| 190 | |||
| 191 | /* max iter */ | ||
| 192 | ✗ | cflags = (char*)omc_flagValue[FLAG_IPOPT_MAX_ITER]; | |
| 193 | ✗ | if(cflags){ | |
| 194 | char buffer[100]; | ||
| 195 | char c; | ||
| 196 | int index_e = -1, i = 0; | ||
| 197 | strcpy(buffer,cflags); | ||
| 198 | |||
| 199 | ✗ | while(buffer[i] != '\0'){ | |
| 200 | ✗ | if(buffer[i] == 'e'){ | |
| 201 | index_e = i; | ||
| 202 | break; | ||
| 203 | } | ||
| 204 | ✗ | ++i; | |
| 205 | } | ||
| 206 | |||
| 207 | ✗ | if(index_e < 0){ | |
| 208 | max_iter = atoi(cflags); | ||
| 209 | ✗ | if(max_iter >= 0) | |
| 210 | ✗ | AddIpoptIntOption(nlp, "max_iter", max_iter); | |
| 211 | printf("\nmax_iter = %i",atoi(cflags)); | ||
| 212 | |||
| 213 | }else{ | ||
| 214 | ✗ | max_iter = (atoi(cflags)*pow(10.0, (double)atoi(cflags+index_e+1))); | |
| 215 | ✗ | if(max_iter >= 0) | |
| 216 | ✗ | AddIpoptIntOption(nlp, "max_iter", (int)max_iter); | |
| 217 | ✗ | printf("\nmax_iter = (int) %i | (double) %g",(int)max_iter, atoi(cflags)*pow(10.0, (double)atoi(cflags+index_e+1))); | |
| 218 | } | ||
| 219 | }else | ||
| 220 | ✗ | AddIpoptIntOption(nlp, "max_iter", 5000); | |
| 221 | |||
| 222 | /*heuristic optition */ | ||
| 223 | { | ||
| 224 | int ws = 0; | ||
| 225 | ✗ | cflags = (char*)omc_flagValue[FLAG_IPOPT_WARM_START]; | |
| 226 | ✗ | if(cflags){ | |
| 227 | ws = atoi(cflags); | ||
| 228 | } | ||
| 229 | |||
| 230 | ✗ | if(ws > 0){ | |
| 231 | ✗ | double shift = pow(10,-1.0*ws); | |
| 232 | ✗ | AddIpoptNumOption(nlp,"mu_init",shift); | |
| 233 | ✗ | AddIpoptNumOption(nlp,"bound_mult_init_val",shift); | |
| 234 | ✗ | AddIpoptStrOption(nlp,"mu_strategy", "monotone"); | |
| 235 | ✗ | AddIpoptNumOption(nlp,"bound_push", 1e-5); | |
| 236 | ✗ | AddIpoptNumOption(nlp,"bound_frac", 1e-5); | |
| 237 | ✗ | AddIpoptNumOption(nlp,"slack_bound_push", 1e-5); | |
| 238 | ✗ | AddIpoptNumOption(nlp,"constr_mult_init_max", 1e-5); | |
| 239 | ✗ | AddIpoptStrOption(nlp,"bound_mult_init_method","mu-based"); | |
| 240 | }else{ | ||
| 241 | ✗ | AddIpoptStrOption(nlp,"mu_strategy","adaptive"); | |
| 242 | ✗ | AddIpoptStrOption(nlp,"bound_mult_init_method","constant"); | |
| 243 | } | ||
| 244 | ✗ | AddIpoptStrOption(nlp,"fixed_variable_treatment","make_parameter"); | |
| 245 | ✗ | AddIpoptStrOption(nlp,"dependency_detection_with_rhs","yes"); | |
| 246 | ✗ | AddIpoptNumOption(nlp,"nu_init",1e-9); | |
| 247 | ✗ | AddIpoptNumOption(nlp,"eta_phi",1e-10); | |
| 248 | } | ||
| 249 | |||
| 250 | /********************************************************************/ | ||
| 251 | |||
| 252 | |||
| 253 | ✗ | if(max_iter >=0){ | |
| 254 | ✗ | optData->iter_ = 0.0; | |
| 255 | ✗ | optData->index = 1; | |
| 256 | ✗ | res = IpoptSolve(nlp, vopt, NULL, &obj, mult_g, mult_x_L, mult_x_U, (void*)optData); | |
| 257 | } | ||
| 258 | ✗ | if(res != 0 && !OMC_ACTIVE_STREAM(OMC_LOG_IPOPT)) | |
| 259 | ✗ | warningStreamPrint(OMC_LOG_STDOUT, 0, "No optimal solution found!\nUse -lv=LOG_IPOPT for more information."); | |
| 260 | ✗ | FreeIpoptProblem(nlp); | |
| 261 | ✗ | return res; | |
| 262 | } | ||
| 263 | |||
| 264 | |||
| 265 | ✗ | static inline void freeOptimizerData(OptData*optData){ | |
| 266 | ✗ | const int nsi = optData->dim.nsi; | |
| 267 | ✗ | const int np = optData->dim.np; | |
| 268 | ✗ | const int nv = optData->dim.nv; | |
| 269 | ✗ | const int nJ = optData->dim.nJ; | |
| 270 | |||
| 271 | int i,j,k; | ||
| 272 | |||
| 273 | /*************************/ | ||
| 274 | ✗ | for(i=0; i < nsi; ++i) | |
| 275 | ✗ | free(optData->time.t[i]); | |
| 276 | ✗ | free(optData->time.t); | |
| 277 | ✗ | free(optData->time.dt); | |
| 278 | /*************************/ | ||
| 279 | ✗ | free(optData->bounds.vmin); | |
| 280 | ✗ | free(optData->bounds.vmax); | |
| 281 | ✗ | free(optData->bounds.Vmin); | |
| 282 | ✗ | free(optData->bounds.Vmax); | |
| 283 | ✗ | free(optData->bounds.vnom); | |
| 284 | ✗ | free(optData->bounds.scalF); | |
| 285 | ✗ | for(i=0; i < nsi; ++i) | |
| 286 | ✗ | free(optData->bounds.scaldt[i]); | |
| 287 | ✗ | free(optData->bounds.scaldt); | |
| 288 | ✗ | for(i = 0; i < nsi; ++i){ | |
| 289 | ✗ | free(optData->bounds.scalb[i]); | |
| 290 | } | ||
| 291 | ✗ | free(optData->bounds.scalb); | |
| 292 | ✗ | free(optData->bounds.u0); | |
| 293 | /*************************/ | ||
| 294 | ✗ | free(optData->ipop.vopt); | |
| 295 | ✗ | free(optData->ipop.gmin); | |
| 296 | ✗ | free(optData->ipop.gmax); | |
| 297 | ✗ | free(optData->ipop.mult_g); | |
| 298 | ✗ | free(optData->ipop.mult_x_L); | |
| 299 | ✗ | free(optData->ipop.mult_x_U); | |
| 300 | /*************************/ | ||
| 301 | |||
| 302 | ✗ | for(k = 0; k < nv; ++k){ | |
| 303 | ✗ | free(optData->s.H0[k]); | |
| 304 | ✗ | free(optData->s.H1[k]); | |
| 305 | ✗ | free(optData->s.Hm[k]); | |
| 306 | ✗ | free(optData->s.Hl[k]); | |
| 307 | } | ||
| 308 | ✗ | free(optData->s.H0); | |
| 309 | ✗ | free(optData->s.H1); | |
| 310 | ✗ | free(optData->s.Hm); | |
| 311 | ✗ | free(optData->s.Hl); | |
| 312 | ✗ | for(j = 0; j < nJ; ++j){ | |
| 313 | ✗ | for(k = 0; k < nv; ++k) | |
| 314 | ✗ | free(optData->s.Hg[j][k]); | |
| 315 | ✗ | free(optData->s.Hg[j]); | |
| 316 | } | ||
| 317 | ✗ | free(optData->s.Hg); | |
| 318 | ✗ | free(optData->s.lindex); | |
| 319 | ✗ | free(optData->s.seedVec); | |
| 320 | ✗ | free(optData->s.indexCon2); | |
| 321 | ✗ | free(optData->s.indexCon3); | |
| 322 | ✗ | free(optData->s.indexJ2); | |
| 323 | ✗ | free(optData->s.indexJ3); | |
| 324 | |||
| 325 | /*************************/ | ||
| 326 | ✗ | for(i = 0; i < nsi; ++i){ | |
| 327 | ✗ | for(j = 0; j < np; ++j){ | |
| 328 | ✗ | free(optData->v[i][j]); | |
| 329 | } | ||
| 330 | ✗ | free(optData->v[i]); | |
| 331 | } | ||
| 332 | ✗ | free(optData->v); | |
| 333 | ✗ | free(optData->v0); | |
| 334 | ✗ | free(optData->sv0); | |
| 335 | ✗ | free(optData->b0); | |
| 336 | ✗ | free(optData->i0); | |
| 337 | ✗ | free(optData->b0Pre); | |
| 338 | ✗ | free(optData->i0Pre); | |
| 339 | ✗ | free(optData->v0Pre); | |
| 340 | ✗ | free(optData->rePre); | |
| 341 | ✗ | free(optData->re); | |
| 342 | ✗ | free(optData->storeR); | |
| 343 | |||
| 344 | ✗ | for(i = 0; i < nsi; ++i){ | |
| 345 | ✗ | for(j = 0; j < np; ++j){ | |
| 346 | ✗ | for(k = 0; k < nJ; ++k) | |
| 347 | ✗ | free(optData->J[i][j][k]); | |
| 348 | ✗ | free(optData->J[i][j]); | |
| 349 | } | ||
| 350 | ✗ | free(optData->J[i]); | |
| 351 | } | ||
| 352 | ✗ | free(optData->J); | |
| 353 | ✗ | for(k = 0; k < nJ; ++k) | |
| 354 | ✗ | free(optData->tmpJ[k]); | |
| 355 | ✗ | for(k = 0; k < nJ; ++k){ | |
| 356 | ✗ | for(j = 0; j < nv; ++j){ | |
| 357 | ✗ | free(optData->H[k][j]); | |
| 358 | } | ||
| 359 | ✗ | free(optData->H[k]); | |
| 360 | } | ||
| 361 | ✗ | free(optData->H); | |
| 362 | ✗ | for(j = 0; j < nv; ++j){ | |
| 363 | ✗ | free(optData->Hl[j]); | |
| 364 | ✗ | free(optData->Hm[j]); | |
| 365 | } | ||
| 366 | ✗ | free(optData->Hl); | |
| 367 | ✗ | free(optData->Hm); | |
| 368 | ✗ | if(optData->dim.updateHessian > 0) | |
| 369 | ✗ | free(optData->oldH); | |
| 370 | |||
| 371 | ✗ | free(optData->dim.inputName); | |
| 372 | |||
| 373 | ✗ | for(k = 0; k < 2; ++k){ | |
| 374 | ✗ | if(optData->s.matrix[2+k]){ | |
| 375 | ✗ | for(i = 0; i< nsi; ++i){ | |
| 376 | ✗ | for(j = 0; j< np; ++j){ | |
| 377 | ✗ | free(optData->dim.analyticJacobians_tmpVars[k][i][j]); | |
| 378 | } | ||
| 379 | ✗ | free(optData->dim.analyticJacobians_tmpVars[k][i]); | |
| 380 | } | ||
| 381 | ✗ | free(optData->dim.analyticJacobians_tmpVars[k]); | |
| 382 | } | ||
| 383 | } | ||
| 384 | ✗ | free(optData->dim.analyticJacobians_tmpVars); | |
| 385 | |||
| 386 | ✗ | for(i = 0; i< optData->dim.nJ; ++i) | |
| 387 | ✗ | free(optData->s.JderCon[i]); | |
| 388 | ✗ | free(optData->s.JderCon); | |
| 389 | ✗ | free(optData->s.gradM); | |
| 390 | ✗ | free(optData->s.gradL); | |
| 391 | ✗ | } | |
| 392 |