$darkmode
DENOPTIM
FitnessExpressionParser.java
Go to the documentation of this file.
1/*
2 * DENOPTIM
3 * Copyright (C) 2022 Marco Foscato <marco.foscato@uib.no>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as published
7 * by the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package denoptim.fitness;
20
21import java.io.File;
22import java.util.ArrayList;
23import java.util.HashMap;
24import java.util.HashSet;
25import java.util.List;
26import java.util.Map;
27import java.util.Set;
28
29import org.openscience.cdk.exception.CDKException;
30import org.openscience.cdk.fingerprint.IBitFingerprint;
31import org.openscience.cdk.fingerprint.IFingerprinter;
32import org.openscience.cdk.fingerprint.ShortestPathFingerprinter;
33import org.openscience.cdk.fingerprint.SubstructureFingerprinter;
34import org.openscience.cdk.interfaces.IAtomContainer;
35import org.openscience.cdk.qsar.IDescriptor;
36import org.openscience.cdk.tools.manipulator.AtomContainerManipulator;
37
38import denoptim.exception.DENOPTIMException;
39import denoptim.fitness.descriptors.TanimotoMolSimilarity;
40import denoptim.fitness.descriptors.TanimotoMolSimilarityBySubstructure;
41import denoptim.io.DenoptimIO;
42import jakarta.el.BeanELResolver;
43import jakarta.el.ELContext;
44import jakarta.el.ELResolver;
45import jakarta.el.ExpressionFactory;
46import jakarta.el.FunctionMapper;
47import jakarta.el.MethodExpression;
48import jakarta.el.ValueExpression;
49import jakarta.el.VariableMapper;
50
55{
61 private List<Variable> variables = new ArrayList<Variable>();
62
67 private List<DescriptorForFitness> descriptors =
68 new ArrayList<DescriptorForFitness>();
69
70//------------------------------------------------------------------------------
71
76
77//------------------------------------------------------------------------------
78
100 public void parse(String fitnessExpression,
101 List<String> customVarDescExpressions) throws DENOPTIMException
102 {
103 // First, parse the definitions of custom variables
104 ExpressionFactory expFactory = ExpressionFactory.newInstance();
106 for (String variableDefinition : customVarDescExpressions)
107 {
108 // NB the replacement of the Windows separator is a workaround for
109 // the fact that the ELParser sees that separator as a special
110 // character that I could not manage to escape. The result of
111 // such interpretation of \ (or '\\' as it is actually returned
112 // from the System.getProperty("file.separator") method) makes
113 // parsing of pathnames as expression components completely wrong.
114 // Below we re-introduce Windows separators, if we need to.
115 boolean useWindowsFileSeparator = variableDefinition.contains("\\");
116 String modVarDef = variableDefinition;
117 if (useWindowsFileSeparator)
118 modVarDef = variableDefinition.replace("\\", "/");
119
120 MethodExpression me = expFactory.createMethodExpression(cvdc,
121 modVarDef, Variable.class, null);
122 Variable v = (Variable) me.invoke(cvdc, null);
123
124 if (useWindowsFileSeparator)
125 {
126 for (int ipar=0; ipar<v.params.length; ipar++)
127 {
128 v.params[ipar] = v.params[ipar].replace("/", "\\");
129 }
130 }
131 variables.add(v);
132 }
133
134 // Then, read the fitness expression to identify all variables in it.
135 // That is, all those that have not been already defined as custom
136 // variables.
138 // NB: Double.class is needed to respect the contract (null cannot be used)
139 expFactory.createValueExpression(ncc, fitnessExpression, Double.class);
140
141 // Then, collects descriptors that are needed to map variable names into
142 // values. Note that one descriptor implementation can be used by more
143 // than one variable,
144 // and the same descriptor implementation (though differently
145 // configured) might be requested by different variables. The latter
146 // situation is not effective here, because the configuration of the
147 // implementations is defined later. Therefore, we get one
148 // descriptor implementation for all its potentially different
149 // configurations.
150 Set<String> descriptorImplementationNames = new HashSet<String>();
151 for (Variable v : variables)
152 {
153 descriptorImplementationNames.add(v.getDescriptorName());
154 }
155 List<DescriptorForFitness> rawDescriptors =
157 descriptorImplementationNames);
158
159 // This map keeps track of which instances are standard, i.e., not
160 // affected by custom parameters.
161 Map<String,DescriptorForFitness> standardDescriptors =
162 new HashMap<String,DescriptorForFitness>();
163
164 // Now we create the list of descriptors, each with either standard or
165 // customized configuration, that will be fed to the fitness provider
166 // descriptor engine. This list goes into the field 'descriptors'.
167 for (int i=0; i<variables.size(); i++)
168 {
169 Variable v = variables.get(i);
170 String varName = v.getName();
171 String descName = v.getDescriptorName();
172
173 // 'raw' means that it has not yet been configured, so it can be
174 // used to draft both standard and customized descriptors instances
175 DescriptorForFitness rawDff = rawDescriptors.stream()
176 .filter(d -> descName.equals(d.getShortName()))
177 .findAny()
178 .orElse(null);
179 if (rawDff==null)
180 throw new DENOPTIMException("Looking for descriptor '"
181 + descName + "' that should be used to produce "
182 + "variable '" + varName
183 + "', but no match found in the list of descriptor "
184 + "implementations. "
185 + "Check the name of the descriptor you are trying to "
186 + "use.");
187
188 if (v.params == null)
189 {
190 // No custom parameter, so we take (or make) a standard
191 // implementation of the descriptor.
192 if (standardDescriptors.containsKey(rawDff.getShortName()))
193 {
194 standardDescriptors.get(rawDff.getShortName())
195 .addDependentVariable(v);
196 } else {
197 DescriptorForFitness dff = rawDff.makeCopy();
199 descriptors.add(dff);
200 standardDescriptors.put(rawDff.getShortName(),dff);
201 }
202 } else {
203 // This variable requires a customized descriptor configuration
204 // So, here a brand new descriptor is made and configured
205 DescriptorForFitness dff = rawDff.makeCopy();
207
208 IDescriptor impl = dff.implementation;
209 String[] parNames = impl.getParameterNames();
210 if (parNames.length != v.params.length)
211 {
212 throw new DENOPTIMException("Wrong number of parameters in "
213 + "configuration of descriptor '"
214 + rawDff.getShortName() + "' for variable '"
215 + varName + "'. Found " + v.params.length + " but "
216 + "the descriptor requires " + parNames.length+".");
217 }
218
219 // Here we configure the descriptor implementation according
220 // to the parameters parsed from the field of the Variable
221 Object[] params = new Object[parNames.length];
222 for (int j=0; j<parNames.length; j++)
223 {
224 Object parType = impl.getParameterType(parNames[j]);
225 if (parType == null)
226 {
227 throw new DENOPTIMException("Descriptor '"
228 + rawDff.getShortName() + "' does not specify "
229 + "the type of a parameter.");
230 }
231 Object p = null;
232 if (parType instanceof Integer){
233 p = Integer.parseInt(v.params[j]);
234 } else if (parType instanceof Double) {
235 p = Double.parseDouble(v.params[j]);
236 } else if (parType instanceof Boolean) {
237 p = Boolean.getBoolean(v.params[j]);
238 } else if (parType instanceof String) {
239 p = v.params[j];
240 } else if (parType instanceof Class) {
241 //TODO Change: this part lacks generality!
242 String type = ((Class<?>) parType).getSimpleName();
243 switch (type)
244 {
245 case "IBitFingerprint":
246 if (impl instanceof TanimotoMolSimilarityBySubstructure)
247 {
248 // WARNING! We expect this to be found always
249 // after the parameter providing a pathname
250 // to a file with substructure smarts.
252 v.params[j],((String[])params[j-1]));
253 } else if (impl instanceof TanimotoMolSimilarity) {
254 // WARNING! we expect this to be found always
255 // after the corresponding IFingerprinter
256 // parameter.
257 p = makeIBitFingerprint(v.params[j],
258 makeIFingerprinter(params[j-1].toString()));
259 } else {
260 throw new DENOPTIMException("Descriptor '"
261 + rawDff.getShortName() + "' is "
262 + "not expected to need an "
263 + "IBitFingerprint parameter.");
264 }
265 break;
266
267 case "String[]":
268 p = makeStringArray(v.params[j]);
269 break;
270
271 default:
272 throw new DENOPTIMException("Parameter '"
273 + parNames[j] + "' for descriptor '"
274 + descName + "' is requested to be of "
275 + "type '" + type + "' but no "
276 + "handling of such type is available "
277 + "in FitnessParameters. Please, "
278 + "report this to the develoment "
279 + "team.");
280 }
281 } else {
282 throw new DENOPTIMException("Parameter '"
283 + parNames[j] + "' for descriptor '"
284 + descName + "' in an instance of a class"
285 + "that is not expected by "
286 + "in FitnessParameters. Please, "
287 + "report this to the develoment team.");
288 }
289 params[j] = p;
290 }
291 try
292 {
293 impl.setParameters(params);
294 } catch (CDKException e)
295 {
296 // This should never happen: type and number of the params
297 // is made to fit the request of this method.
298 throw new DENOPTIMException("Wrong set of parameters "
299 + "for descriptor '" + descName
300 + "in FitnessParameters. Please, "
301 + "report this to the develoment team.",e);
302 }
303 descriptors.add(dff);
304 }
305 }
306 }
307
308//------------------------------------------------------------------------------
309
315 private class CustomVariableDefiningContext extends ELContext
316 {
317 private FunctionMapper fm = null;
318
322 private VariableMapper vm = new VariableMapper() {
323 @SuppressWarnings("serial")
324 @Override
325 public ValueExpression resolveVariable(String variable) {
326 ValueExpression ve = new ValueExpression() {
327
328 @Override
329 public Object getValue(ELContext context)
330 {
331 Variable v = new Variable("BlankVariable");
332 return v;
333 }
334
335 @Override
336 public void setValue(ELContext context, Object value)
337 {
338 //NOPE!
339 }
340
341 @Override
342 public boolean isReadOnly(ELContext context)
343 {
344 return true;
345 }
346
347 @Override
348 public Class<?> getType(ELContext context)
349 {
350 return Variable.class;
351 }
352
353 @Override
354 public Class<?> getExpectedType()
355 {
356 return Variable.class;
357 }
358
359 @Override
360 public String getExpressionString()
361 {
362 return null;
363 }
364
365 @Override
366 public boolean equals(Object obj)
367 {
368 return false;
369 }
370
371 @Override
372 public int hashCode()
373 {
374 return 0;
375 }
376
377 @Override
378 public boolean isLiteralText()
379 {
380 return false;
381 }};
382 return ve;
383 }
384
385 @Override
386 public ValueExpression setVariable(String variable,
387 ValueExpression expression) {
388 return null;
389 }
390 };
391
395 private ELResolver resolver = new BeanELResolver();
396
397 @Override
398 public ELResolver getELResolver()
399 {
400 return resolver;
401 }
402
403 @Override
404 public FunctionMapper getFunctionMapper()
405 {
406 return fm;
407 }
408
409 @Override
410 public VariableMapper getVariableMapper()
411 {
412 return vm;
413 }
414 }
415
416//------------------------------------------------------------------------------
417
423 private class VariableDefiningContext extends ELContext
424 {
428 private List<Variable> variables;
429
430 private FunctionMapper fm = null;
431
440 private VariableMapper vm = new VariableMapper() {
445 @Override
446 public ValueExpression resolveVariable(String variableName) {
447 for (Variable existing : variables)
448 {
449 if (variableName.equals(existing.getName()))
450 return null;
451 }
452 Variable v = new Variable(variableName);
453 v.setDescriptorName(variableName);
454 variables.add(v);
455 return null;
456 }
457
458 @Override
459 public ValueExpression setVariable(String variable,
460 ValueExpression expression) {
461 return null;
462 }
463 };
464
468 private ELResolver resolver = new BeanELResolver();
469
475 public VariableDefiningContext(List<Variable> variables)
476 {
477 this.variables = variables;
478 }
479
480 @Override
481 public ELResolver getELResolver()
482 {
483 return resolver;
484 }
485
486 @Override
487 public FunctionMapper getFunctionMapper()
488 {
489 return fm;
490 }
491
492 @Override
493 public VariableMapper getVariableMapper()
494 {
495 return vm;
496 }
497 }
498
499//------------------------------------------------------------------------------
500
501 private String[] makeStringArray(String line) throws DENOPTIMException
502 {
503 String key = "FILE:";
504 line = line.trim();
505 if (!line.toUpperCase().startsWith(key))
506 {
507 throw new DENOPTIMException("Presently, parameters of type "
508 + "'String[]' can only be generated upon "
509 + "reading a text lines from file. To this end, the "
510 + "definition of the parameter *MUST* start with '"
511 + key + "'. Input line '" + line + "' does not.");
512 }
513
514 String fileName = line.substring(key.length()).trim();
515 ArrayList<String> lst = DenoptimIO.readList(fileName);
516 String[] arr = new String[lst.size()];
517 arr = lst.toArray(arr);
518 return arr;
519 }
520
521//------------------------------------------------------------------------------
522
527 private IBitFingerprint makeIBitFingerprintBySubstructure(String line,
528 String[] smarts) throws DENOPTIMException
529 {
530 IFingerprinter fingerprinter = new SubstructureFingerprinter(smarts);
531 return makeIBitFingerprint(line, fingerprinter);
532 }
533
534//------------------------------------------------------------------------------
535
540 private IBitFingerprint makeIBitFingerprint(String line,
541 IFingerprinter fingerprinter) throws DENOPTIMException
542 {
543 String key = "FILE:";
544 line = line.trim();
545 if (!line.toUpperCase().startsWith(key))
546 {
547 throw new DENOPTIMException("Presently, parameters of type "
548 + "'IBitFingerprint' can only be generated upon "
549 + "reading a molecule from file. To this end, the "
550 + "definition of the parameter *MUST* start with '"
551 + key + "'. Input line '" + line + "' does not.");
552 }
553 if (fingerprinter == null)
554 {
555 throw new IllegalArgumentException("ERROR! Fingerprinter should be "
556 + "created before attempting generation of a fingerprint.");
557 }
558 String fileName = line.substring(key.length()).trim();
559 IBitFingerprint fp = null;
560 try
561 {
562 IAtomContainer refMol = DenoptimIO.readAllAtomContainers(
563 new File(fileName)).get(0);
564
565 if (fingerprinter instanceof ShortestPathFingerprinter)
566 {
567 try
568 {
569 AtomContainerManipulator.percieveAtomTypesAndConfigureAtoms(
570 refMol);
571 } catch (CDKException e1)
572 {
573 throw new DENOPTIMException("Could not assign atom types "
574 + "to calculate fingerprint of reference molecule.",
575 e1);
576 }
577 }
578
579 fp = fingerprinter.getBitFingerprint(refMol);
580 } catch (Exception e)
581 {
582 throw new DENOPTIMException("ERROR! Unable to read molecule from "
583 + "file '" + fileName + "'.",e);
584 }
585 return fp;
586 }
587
588//------------------------------------------------------------------------------
589
590 private IFingerprinter makeIFingerprinter(String classShortName)
591 throws DENOPTIMException
592 {
593 IFingerprinter fp = null;
594 try
595 {
596 fp = TanimotoMolSimilarity.makeIFingerprinter(classShortName);
597 } catch (Throwable t)
598 {
599 throw new DENOPTIMException("Could not make new instance of '"
600 + classShortName + "'.", t);
601 }
602 return fp;
603 }
604
605//------------------------------------------------------------------------------
606
611 public List<Variable> getVariables()
612 {
613 return variables;
614 }
615
616//------------------------------------------------------------------------------
617
624 public List<DescriptorForFitness> getDescriptors()
625 {
626 return descriptors;
627 }
628
629//------------------------------------------------------------------------------
630
631}
This is a reference to a specific descriptor value.
IDescriptor implementation
Implementation of the descriptor's calculator.
DescriptorForFitness makeCopy()
Copy this descriptor and created an independent instance of the underlying descriptor implementation.
void addDependentVariable(Variable v)
Append the reference to a variable that used data produced by the calculation of this descriptor.
static List< DescriptorForFitness > findAllDescriptorImplementations(Set< String > requiredDescriptors)
Searches for descriptor implementations.
In this context we only need to know that "Variable" is mapped to the class Variable.
VariableMapper vm
Mapper that always returns a new blank instance of Variable.
This is the context on which we read a fitness expression, and parse it to identify all variables in ...
VariableMapper vm
This mapper does not map variables, but it creates an instance of Variable named with the given strin...
List< Variable > variables
Reference to where we collect all the created variables.
VariableDefiningContext(List< Variable > variables)
Constructor specifying the destination of the variables created when parsing the expression.
Class parsing fitness expression by means of Expression Language.
List< DescriptorForFitness > descriptors
The list of descriptors needed to calculate the variables that are used to calculate the fitness with...
List< DescriptorForFitness > getDescriptors()
Returns the list of descriptors needed to compute the numerical values of the variables in the expres...
List< Variable > variables
List of variables used in the calculation of the fitness.
void parse(String fitnessExpression, List< String > customVarDescExpressions)
Parses the given expressions.
IBitFingerprint makeIBitFingerprintBySubstructure(String line, String[] smarts)
For now we only accept a filename from which we read in a molecule.
IFingerprinter makeIFingerprinter(String classShortName)
List< Variable > getVariables()
Returns the list of variables in the expression of the fitness.
IBitFingerprint makeIBitFingerprint(String line, IFingerprinter fingerprinter)
For now we only accept a filename from which we read in a molecule.
A variable in the expression defining the fitness.
Definition: Variable.java:42
String[] params
Definition of custom parameters for the configuration of the descriptor implementation.
Definition: Variable.java:63
String getName()
Get the name of this variable, i.e., the string used in the fitness-defining expression.
Definition: Variable.java:132
String getDescriptorName()
Get the short name of the descriptor implementation.
Definition: Variable.java:143
void setDescriptorName(String descName)
Set the short name of the descriptor implementation.
Definition: Variable.java:154
Calculates the molecular similarity against a target compound the fingerprint of which is given as pa...
Calculates the molecular similarity against a target compound the fingerprint of which is given as pa...
static IFingerprinter makeIFingerprinter(String classShortName)
Utility methods for input/output.
static ArrayList< String > readList(String fileName)
Read list of data as text.
static List< IAtomContainer > readAllAtomContainers(File file)
Returns a single collection with all atom containers found in a file of any format.