Source code for schrodinger.application.desmond.license
"""
Module for license related code in Desmond workflows.
Copyright Schrodinger LLC, All Rights Reserved.
"""
import getpass
import json
import os
from pathlib import Path
from typing import Dict
from typing import List
import requests
from schrodinger.application.desmond import bld_def
from schrodinger.application.desmond import bld_ver
from schrodinger.infra import licensing
from schrodinger.job import jobcontrol
from schrodinger import gpgpu
from schrodinger.application.desmond.constants import PRODUCT
MODEL2_ACCESS_FILE = Path(os.environ["SCHRODINGER"], "config",
"per_compound.json")
MODEL2_ENV_NAME = 'SCHRODINGER_PER_COMPOUND_LICENSING'
DESMOND_GPU_TOKEN_MULTIPLIER = licensing.DESMOND_GPU_TOKEN_MULTIPLIER
[docs]class Model2CheckoutError(Exception):
pass
[docs]def add_fep_lic(args: List[str]) -> List[str]:
"""
Add the command line arguments to properly handle the FEP GPU license.
NOTE: If the same license is present with a different
number of tokens, this will override the current value.
:param args: List of command line arguments. This function will look into
`args` for the -HOST and -cpu options to determine the number of processors.
If these are not found, this function will assume one processor.
:param use_custom_charges: Set to True when using custom charges (F16).
:return: The updated command.
"""
return _deduplicate_lics(args + ["-lic", fep_lic(get_host(args))])
[docs]def add_md_lic(args: List[str]) -> List[str]:
"""
Add the command line arguments to properly handle the
MD GPU license.
NOTE: If the same license is present with a different
number of tokens, this will override the current value.
:param args: List of command line arguments. This function will look into
`args` for the -HOST and -cpu options to determine the number of processors.
If these are not found, this function will assume one processor.
:return: The updated command.
"""
return _deduplicate_lics(args + ["-lic", md_lic(get_host(args))])
[docs]def add_watermap_lic(args: List[str]) -> List[str]:
"""
Add the command line arguments to properly handle the
Watermap GPU license.
NOTE: If the same license is present with a different
number of tokens, this will override the current value.
:param args: List of command line arguments. This function will look into
`args` for the -HOST and -cpu options to determine the number of processors.
If these are not found, this function will assume one processor.
:return: The updated command.
"""
return _deduplicate_lics(args + ["-lic", watermap_lic(get_host(args))])
[docs]def protein_mutation_lics() -> List[str]:
"""
Return the license strings needed for Plop/Macromodel, used in the
protein mutation generator stage.
"""
return [
f"{licensing.getFeatureName(licensing.PSP_PLOP)}:8",
f'{licensing.getFeatureName(licensing.MMOD_MACROMODEL)}:2'
]
[docs]def epik_lic() -> str:
"""
Return the license string needed for Epik, used in constant pH.
"""
return f'{licensing.getFeatureName(licensing.EPIK_MAIN)}:1'
[docs]def fep_lic(host: str) -> str:
"""
Return the license string for FEP GPU, given the -HOST value.
"""
license_string = licensing.getFeatureName(licensing.FEP_GPGPU)
multiplier = gpgpu.get_scaled_token_count(_get_hostname(host),
DESMOND_GPU_TOKEN_MULTIPLIER)
return f"{license_string}:{_get_num_procs(host) * multiplier}"
[docs]def md_lic(host: str) -> str:
"""
Return the license string for MD GPU, given the -HOST value.
"""
license_string = licensing.getFeatureName(
licensing.DESMOND_ACADEMIC) if _is_academic(
) else licensing.getFeatureName(licensing.DESMOND_GPGPU)
multiplier = gpgpu.get_scaled_token_count(_get_hostname(host),
DESMOND_GPU_TOKEN_MULTIPLIER)
return f"{license_string}:{_get_num_procs(host) * multiplier}"
[docs]def watermap_lic(host: str) -> str:
"""
Return the license string for Watermap GPU, given the -HOST value.
"""
license_string = licensing.getFeatureName(licensing.DESMOND_WATERMAP_GPGPU)
multiplier = gpgpu.get_scaled_token_count(_get_hostname(host),
DESMOND_GPU_TOKEN_MULTIPLIER)
return f"{license_string}:{_get_num_procs(host) * multiplier}"
def _get_model2_filename():
"""
Returns the model2 filename from the environment variable,
or None if it's not defined.
"""
return os.getenv(MODEL2_ENV_NAME)
def _post_model2(endpoint: str, additional_data: Dict = None):
access_fname = _get_model2_filename()
if not access_fname:
raise Model2CheckoutError(
'Could not find FEP+ Model 2 configuration file.')
params = json.loads(Path(access_fname).read_text())
url = params['server']
params.update(additional_data or {})
return requests.post(f"{url}/{endpoint}", json=params)
[docs]def has_model2_file() -> bool:
access_fname = _get_model2_filename()
if not access_fname:
return False
return Path(access_fname).exists()
[docs]def is_model2_server_available() -> bool:
try:
return _post_model2('ping').ok
except Exception:
# Any exception here means we couldn't connect
return False
[docs]def checkout_model2_compounds(num_compounds: int, product: PRODUCT):
"""
Checkout the specified number of model2 compounds.
:param product: Name of the product to use.
"""
if product == PRODUCT.IFD_MD:
for _ in range(num_compounds):
licensing.checkout_ifd_md_completed_compound()
else:
licensing.checkout_fep_completed_compound(num_compounds)
access_fname = _get_model2_filename()
if not access_fname:
return
if not has_model2_file():
return
if not is_model2_server_available():
raise Model2CheckoutError(
'Need to make sure per compound server is accessible to run jobs. Please contact support.'
)
data = {}
try:
data['username'] = getpass.getuser()
except Exception:
# A number of different exceptions could be raised
# so just catch them all.
data['username'] = ''
data['num_compounds'] = num_compounds
data['product'] = product
try:
_post_model2('checkout', data)
except Exception as e:
raise Model2CheckoutError(
f'Could not checkout compounds: {e}. Please contact support.')
[docs]def get_host(args: List[str]) -> str:
"""
Return the value for the -HOST argument if found,
otherwise default to 'localhost:1'.
"""
if '-HOST' in args:
return args[args.index('-HOST') + 1]
return 'localhost:1'
def _get_num_procs(host: str) -> int:
"""
Return the number of processors (GPUs or CPUs) from command line
arguments.
"""
return jobcontrol.host_str_to_list(host)[0][-1] or 1
def _get_hostname(host: str) -> str:
"""
Return just the host name from a string 'host:number'.
"""
return jobcontrol.host_str_to_list(host)[0][0]
def _is_academic() -> bool:
"""
Return True if this is an academic build. False otherwise.
"""
return bld_ver.desmond_build_version() == bld_def.bld_types[
bld_def.DESMOND_ACADEMIC]
def _deduplicate_lics(args: List[str]) -> List[str]:
"""
Go through the command line arguments `args`
and return the arguments without duplicate licenses.
NOTE: If the same license is present with a different
number of tokens, the one that appears last in the list
will be used.
"""
lic_map = {}
updated_args = []
i = 0
while i < len(args):
if args[i] == '-lic':
lic_value = args[i + 1]
lic_map[lic_value.split(':')[0]] = ['-lic', lic_value]
# Skip -lic value
i += 1
else:
updated_args.append(args[i])
i += 1
return sum(sorted(lic_map.values()), updated_args)