diff --git a/.gitignore b/.gitignore index b31e3e1..e5479ac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/.DS_Store # User generated files +**/temp/ **/design-*.json **/toy_*.json diff --git a/citrination_api_examples/clients_sequence/1_data_client_api_tutorial.ipynb b/citrination_api_examples/clients_sequence/1_data_client_api_tutorial.ipynb index feb8e27..aec4c45 100644 --- a/citrination_api_examples/clients_sequence/1_data_client_api_tutorial.ipynb +++ b/citrination_api_examples/clients_sequence/1_data_client_api_tutorial.ipynb @@ -15,7 +15,7 @@ "\n", "*Authors: Enze Chen, Max Gallant*\n", "\n", - "In this notebook, we will cover how to use the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/) to upload and manage datasets on Citrination. Getting your data on Citrination will allow you to keep your data organized in one place and enable you to perform machine learning (ML) on the data. The application program interface (API) aims to facilitate the process for those who prefer writing Python scripts and wish to avoid the web user interface (UI). As a sanity check, however, it might be helpful for you to keep the UI open and follow along with the tutorial to verify the changes are what you expect." + "In this notebook, we will cover how to use the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/) to upload and manage datasets on Citrination. Getting your data on Citrination will allow you to keep your data organized in one place and enable you to perform machine learning (ML) on the data. The application program interface (API) aims to facilitate the process for those who prefer writing Python scripts. " ] }, { @@ -78,9 +78,9 @@ "outputs": [], "source": [ "# Standard packages\n", - "import os\n", - "import time\n", - "import uuid # generating random IDs\n", + "from os import environ # get environment variables\n", + "from time import sleep # wait time\n", + "from uuid import uuid4 # generating random IDs\n", "\n", "# Third-party packages\n", "from citrination_client import *" @@ -94,11 +94,11 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", - "Assuming that this is the very first time you're interacting with the Citrination API, we will first go over how to properly initialize the client that handles all communication. Most APIs require a key for access, and the PyCC is no exception. You can find your API key by navigating to [Citrination](https://citrination.com), clicking your username in the top-right corner, clicking \"Account Settings,\" and then looking under your Email. Copy this key to your clipboard.\n", + "Assuming that this is the very first time you're interacting with the Citrination API, we will first go over how to properly initialize the client that handles all communication. Most APIs require a key for access, and the PyCC is no exception. You can find your API key by navigating to [Citrination](https://citrination.com), clicking your username in the top-right corner, clicking \"Account Settings,\" and then looking under your Email. Copy this key to your clipboard (`Ctrl+C`).\n", "\n", "Since the key is linked to your specific user profile, *you should never hard-code or expose your API key in your code.* Instead, first store the API key in your [environment variables](https://medium.com/@himanshuagarwal1395/setting-up-environment-variables-in-macos-sierra-f5978369b255) like so (for Macs):\n", "* In Terminal, type `vim ~/.bash_profile` (or use an editor of your choice).\n", - "* In that file, press `i` (edit mode) and add the line `export CITRINATION_API_KEY=\"your_api_key\"`.\n", + "* In that file, press `i` (edit mode) and add the line `export CITRINATION_API_KEY=\"paste_your_api_key\"`.\n", "* Save and exit (`Esc`, `:wq`, `Enter`).\n", "* Open up a new Terminal and load this notebook one more time.\n", "\n", @@ -113,8 +113,8 @@ "metadata": {}, "outputs": [], "source": [ - "site = \"https://citrination.com\" # site you want to access; we'll use the public site\n", - "client = CitrinationClient(api_key=os.environ.get('CITRINATION_API_KEY'), \n", + "site = \"https://citrination.com\" # site you want to access; we'll use the public site\n", + "client = CitrinationClient(api_key=environ.get('CITRINATION_API_KEY'), \n", " site=site)\n", "client # reveal the attributes" ] @@ -136,7 +136,7 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", - "Once the base client is initialized, the [`DataClient`](http://citrineinformatics.github.io/python-citrination-client/tutorial/data_examples.html) can be easily accessed as an attribute." + "Once the base client is initialized, the [`DataClient`](http://citrineinformatics.github.io/python-citrination-client/tutorial/data_examples.html) can be easily accessed using the `.data` attribute." ] }, { @@ -146,7 +146,7 @@ "outputs": [], "source": [ "data_client = client.data\n", - "data_client # reveal the methods" + "data_client # reveal the methods" ] }, { @@ -154,10 +154,10 @@ "metadata": {}, "source": [ "### Create a dataset\n", - "Before you can upload data, you have to create an empty dataset to store the files in. The `create_dataset()` method does exactly this and returns a [`Dataset`](http://citrineinformatics.github.io/python-citrination-client/modules/data/datasets.html) object. The method has the following inputs:\n", - "* **name**: A string for the name of the dataset. It cannot be the same as that of an existing dataset that you own.\n", - "* **description**: A string for the description of the dataset.\n", - "* **public**: A Boolean indicating whether to make the dataset public (`default=False`)." + "Before you can upload data, you have to create an empty dataset to store the files in. The `create_dataset()` method of the `DataClient` does exactly this and returns a [`Dataset`](http://citrineinformatics.github.io/python-citrination-client/modules/data/datasets.html) object. The method has the following inputs:\n", + "* `name`: A string for the name of the dataset. It cannot be the same as that of an existing dataset that you own.\n", + "* `description`: A string for the description of the dataset.\n", + "* `public`: A Boolean indicating whether to make the dataset public (`default=False`)." ] }, { @@ -168,16 +168,17 @@ }, "outputs": [], "source": [ - "data_name = 'PyCC Dataset ' + str(uuid.uuid4())[:6]\n", + "data_name = 'PyCC Dataset ' + str(uuid4())[:6]\n", "data_desc = 'This dataset was created by the PyCC API tutorial.'\n", - "dataset = data_client.create_dataset(name=data_name, description=data_desc)" + "dataset = data_client.create_dataset(name=data_name, \n", + " description=data_desc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Once you've created the `Dataset` object, you can obtain from its attributes the dataset ID, which you will need for subsequent operations." + "Once you've created the `Dataset` object, you can obtain the dataset ID from the `.id` attribute of a `Dataset`. You will need this ID for subsequent operations." ] }, { @@ -192,15 +193,22 @@ "print('It can be accessed at {}/datasets/{}'.format(site, dataset_id))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you click on the above URL, it will take you to the dataset on Citrination, which at this point should be empty." + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Upload data to a dataset\n", - "The `upload()` method allows you to upload a file or a directory to a dataset. The method has the following inputs:\n", - "* **dataset_id**: The integer value of the ID of the dataset to which you will be uploading data.\n", - "* **source_path**: The path to the file or directory you want to upload.\n", - "* **dest_path**: The name of the file or directory as it should appear on Citrination (`default=None`).\n", + "The `upload()` method of the `DataClient` allows you to upload a file or a directory to a dataset. The method has the following inputs:\n", + "* `dataset_id`: The integer value of the ID of the dataset to which you will be uploading data.\n", + "* `source_path`: The path to the file or directory you want to upload.\n", + "* `dest_path`: The name of the file or directory as it should appear on Citrination (`default=None`).\n", "\n", "The returned [`UploadResult`](http://citrineinformatics.github.io/python-citrination-client/modules/data/data_client.html#citrination_client.data.upload_result.UploadResult) object tracks the number of successful and failed uploads. You can also use the function `get_ingest_status()` to check the status of ingest.\n", "\n", @@ -214,22 +222,32 @@ "outputs": [], "source": [ "# Upload a single file\n", - "upload_result = data_client.upload(dataset_id=dataset_id, source_path='test_pif.json')\n", + "upload_result = data_client.upload(dataset_id=dataset_id, \n", + " source_path='test_pif.json')\n", "print('Successful upload? {}'.format(upload_result.successful())) # Boolean; True if none fail\n", "\n", - "# Upload a directory; each file is recursively added and has the folder name as prefix\n", - "upload_result = data_client.upload(dataset_id=dataset_id, source_path='test_pif_dir/')\n", + "# Upload a directory; each file is recursively added and has the folder name as a prefix\n", + "upload_result = data_client.upload(dataset_id=dataset_id, \n", + " source_path='test_pif_dir/')\n", "print('Number of successful uploads: {}'.format(len(upload_result.successes))) # list of successful files\n", "\n", "# Check ingest status with loop\n", - "while (True):\n", - " ingest_status = data_client.get_ingest_status(dataset_id)\n", + "while True:\n", + " ingest_status = data_client.get_ingest_status(dataset_id=dataset_id)\n", " if (ingest_status == 'Finished'):\n", " print('Ingestion complete!')\n", + " print('Dataset URL: {}/datasets/{}'.format(site, dataset_id))\n", " break\n", " else:\n", " print('Waiting for data ingest...')\n", - " time.sleep(10)" + " sleep(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Verify**: If you go back to the dataset in the UI and refresh the page, you should find it populated with PIF records!" ] }, { @@ -238,10 +256,10 @@ "source": [ "### Retrieving data: File download URLs\n", "The more common way to retrieve data from datasets on Citrination is to request download URLs. The `get_dataset_files()` function can be used to get a list of [`DatasetFile`](http://citrineinformatics.github.io/python-citrination-client/modules/data/datasets.html#citrination_client.data.dataset_file.DatasetFile) objects from a dataset. The method has the following inputs:\n", - "* **dataset_id**: The integer value of the ID of the dataset that you're retrieving data from.\n", - "* **glob**: A [regex](https://ryanstutorials.net/regular-expressions-tutorial/) used to select one or more files in the dataset (`default='.'`).\n", - "* **is_dir**: A Boolean indicating whether or not the supplied pattern should be treated as a directory to search in (`default=False`).\n", - "* **version_number**: The integer value of the version number of the dataset to retrieve files from (`default=None`)." + "* `dataset_id`: The integer value of the ID of the dataset that you're retrieving data from.\n", + "* `glob`: A [regex](https://ryanstutorials.net/regular-expressions-tutorial/) used to select one or more files in the dataset (`default='.'`).\n", + "* `is_dir`: A Boolean indicating whether or not the supplied pattern should be treated as a directory to search in (`default=False`).\n", + "* `version_number`: The integer value of the version number of the dataset to retrieve files from (`default=None`)." ] }, { @@ -250,9 +268,12 @@ "metadata": {}, "outputs": [], "source": [ - "regex = 'pif' # matches files with 'pif' in the name\n", - "dataset_files = data_client.get_dataset_files(dataset_id, glob=regex)\n", - "print('The regex \\'{}\\' matched {} files in dataset {}.'.format(regex, len(dataset_files), dataset_id))" + "regex = 'pif' # matches files with 'pif' in the name\n", + "dataset_files = data_client.get_dataset_files(dataset_id=dataset_id, \n", + " glob=regex)\n", + "print('The regex \\'{}\\' matched {} files in dataset {}.'.format(regex, \n", + " len(dataset_files), \n", + " dataset_id))" ] }, { @@ -260,8 +281,8 @@ "metadata": {}, "source": [ "[`DatasetFile`](http://citrineinformatics.github.io/python-citrination-client/modules/data/datasets.html#citrination_client.data.dataset_file.DatasetFile) objects have `path` and `url` attributes that can then be accessed. There is also a `download_files()` method with the following parameters:\n", - "* **dataset_files**: A list of `DatasetFile` objects.\n", - "* **destination**: The path to the desired local download destination (`default='.'`)." + "* `dataset_files`: A list of `DatasetFile` objects.\n", + "* `destination`: The path to the desired local download destination (`default='.'`)." ] }, { @@ -273,7 +294,8 @@ "print('The first file in the dataset is \"{}\"'.format(dataset_files[0].path))\n", "\n", "# Download all files, preserving the same file organization\n", - "data_client.download_files(dataset_files, destination='./downloads/')" + "data_client.download_files(dataset_files=dataset_files, \n", + " destination='./downloads/')" ] }, { @@ -281,10 +303,10 @@ "metadata": {}, "source": [ "### Retrieving data: PIF retrieval\n", - "Another way to retrieve data is to request the contents of a single PIF record in JSON format. The `get_pif()` method takes in the following parameters and returns a [pypif](https://github.com/CitrineInformatics/pypif) [PIF](http://citrineinformatics.github.io/pif-documentation/schema_definition/index.html) object.\n", - "* **dataset_id**: The integer value of the ID of the dataset that you're retrieving data from.\n", - "* **uid**: A string representing the uid of the PIF to retrieve.\n", - "* **dataset_version**: The integer value of the version number of the dataset to retrieve files from (`default=None`).\n", + "Another way to retrieve data is to request the contents of a single PIF record in JSON format. The `get_pif()` method takes in the following parameters and returns a [pypif](https://github.com/CitrineInformatics/pypif) [`pif`](http://citrineinformatics.github.io/pif-documentation/schema_definition/index.html) object.\n", + "* `dataset_id`: The integer value of the ID of the dataset that you're retrieving data from.\n", + "* `uid`: A string representing the uid of the PIF to retrieve.\n", + "* `dataset_version`: The integer value of the version number of the dataset to retrieve files from (`default=None`).\n", "\n", "*Note*: Because the `uid` is only revealed through the web UI and [`SearchClient`](http://citrineinformatics.github.io/python-citrination-client/tutorial/search_examples.html), `get_pif()` is not commonly used when working solely with the `DataClient`." ] @@ -295,8 +317,9 @@ "metadata": {}, "outputs": [], "source": [ - "pif_uid = 'test_uid' # this was set in the PIF\n", - "my_pif = data_client.get_pif(dataset_id, pif_uid)\n", + "pif_uid = 'test_uid' # this UID was set in the PIF\n", + "my_pif = data_client.get_pif(dataset_id=dataset_id, \n", + " uid=pif_uid)\n", "print('The chemical formula of this PIF is {}.'.format(my_pif.chemical_formula))" ] }, @@ -306,10 +329,10 @@ "source": [ "### Modify a dataset\n", "You can easily modify datasets on Citrination with the `update_dataset()` function. It takes as inputs:\n", - "* **dataset_id**: The integer value of the ID of the dataset that you're retrieving data from.\n", - "* **name**: A string for the new name of the dataset (`default=None`).\n", - "* **description**: A string for the new description of the dataset (`default=None`).\n", - "* **public**: A Boolean indicating whether the dataset should be public (`default=None`)." + "* `dataset_id`: The integer value of the ID of the dataset that you're retrieving data from.\n", + "* `name`: A string for the new name of the dataset (`default=None`).\n", + "* `description`: A string for the new description of the dataset (`default=None`).\n", + "* `public`: A Boolean indicating whether the dataset should be public (`default=None`)." ] }, { @@ -318,10 +341,12 @@ "metadata": {}, "outputs": [], "source": [ - "new_name = 'PyCC Dataset New Name ' + str(uuid.uuid4())[:6]\n", + "new_name = 'PyCC Dataset New Name ' + str(uuid4())[:6]\n", "public_flag = False\n", - "new_dataset = data_client.update_dataset(dataset_id, name=new_name, public=public_flag)\n", - "print('Dataset {} is now named \"{}.'.format(dataset_id, new_dataset.name))" + "new_dataset = data_client.update_dataset(dataset_id=dataset_id, \n", + " name=new_name, \n", + " public=public_flag)\n", + "print('Dataset {} is now named \"{}.\"'.format(dataset_id, new_dataset.name))" ] }, { @@ -337,14 +362,15 @@ "metadata": {}, "outputs": [], "source": [ - "print('Files list: {0}.'.format(data_client.list_files(dataset_id, glob='.')))" + "print('Files list: {0}.'.format(data_client.list_files(dataset_id=dataset_id, \n", + " glob='.')))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `create_dataset_version()` function creates a new version of a data set. Note that creating a new version deletes all records from the old version, so handle with care!" + "The `create_dataset_version()` method of the `DataClient` creates a new version of a data set. Note that creating a new version deletes all records from the old version, so handle with care!" ] }, { @@ -353,10 +379,28 @@ "metadata": {}, "outputs": [], "source": [ - "dataset_version = data_client.create_dataset_version(dataset_id)\n", + "dataset_version = data_client.create_dataset_version(dataset_id=dataset_id)\n", "print('Dataset {} is now version {}.'.format(dataset_id, dataset_version.number))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Delete a dataset\n", + "\n", + "Finally, if you wish to delete a dataset that you own, you can use the `delete_dataset()` method of the `DataClient`. As this is a permanent deletion, please handle with care!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# data_client.delete_dataset(dataset_id=dataset_id)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -370,7 +414,8 @@ "* How to create a new dataset.\n", "* How to upload data to the dataset.\n", "* How to retrieve data from the dataset.\n", - "* How to modify the properties of the dataset." + "* How to modify the properties of the dataset.\n", + "* How to delete a dataset." ] }, { diff --git a/citrination_api_examples/clients_sequence/2_data_views_client_api_tutorial.ipynb b/citrination_api_examples/clients_sequence/2_data_views_client_api_tutorial.ipynb index d8773ce..5037593 100644 --- a/citrination_api_examples/clients_sequence/2_data_views_client_api_tutorial.ipynb +++ b/citrination_api_examples/clients_sequence/2_data_views_client_api_tutorial.ipynb @@ -15,7 +15,7 @@ "\n", "*Authors: Enze Chen, Eric Lundberg*\n", "\n", - "In this notebook, we will cover how to *create* a data view using the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/). Data views provide the configuration necessary in order to perform machine learning and identify relationships in your data. We will demonstrate this functionality using the [Band gaps from Strehlow and Cook](https://citrination.com/datasets/1160/show_search?searchMatchOption=fuzzyMatch) dataset, where we will create a view mapping: \n", + "In this notebook, we will cover how to *create* a data view using the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/). Data views provide the configuration necessary in order to perform machine learning and data analysis. We will demonstrate this functionality using the [Band gaps from Strehlow and Cook](https://citrination.com/datasets/1160/show_search?searchMatchOption=fuzzyMatch) dataset, where we will create a view mapping: \n", "\n", "$$\\text{Chemical formula (inorganic) + Crystallinity (categorical)} \\longrightarrow \\boxed{\\text{ML model}} \\longrightarrow \\text{Band gap (real)}$$" ] @@ -79,10 +79,9 @@ "outputs": [], "source": [ "# Standard packages\n", - "import json\n", - "import os\n", - "import time\n", - "import uuid # generating random IDs\n", + "from os import environ # get environment variables\n", + "from time import sleep # wait time\n", + "from uuid import uuid4 # generating random IDs\n", "\n", "# Third-party packages\n", "from citrination_client import *\n", @@ -97,12 +96,18 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", - "The [`DataViewBuilder`](http://citrineinformatics.github.io/python-citrination-client/modules/views/ml_config_builder.html) class handles the configuration for data views and returns a **configuration** object that is an input for the `DataViewsClient`. The configuration specifies the datasets, model, and descriptors. Some of the important parameters to note are:\n", - "* **dataset_ids**: An array of strings, one for each dataset ID that should be included in the view.\n", - "* **descriptors**: A descriptor instance, which could be `{RealDescriptor, InorganicDescriptor, OrganicDescriptor, CategoricalDescriptor,` or `AlloyCompositionDescriptor}`.\n", - " * **Note 1**: Chemical formulas for the API take the key `formula`.\n", - " * **Note 2**: Properties take the key `Property `.\n", - "* **roles**: A role for each descriptor, as a string, which could be `{input, output, latentVariable, ignored}`." + "The [`DataViewBuilder`](http://citrineinformatics.github.io/python-citrination-client/modules/views/ml_config_builder.html) class handles the configuration for data views and returns a **configuration** object that is an input for the `DataViewsClient`. The configuration specifies:\n", + "* The datasets you want to include.\n", + "* The ML model you want to use.\n", + "* Which properties you want to use as descriptors. \n", + "\n", + "Some of the important parameters to note are:\n", + "* `dataset_ids`: An array of strings, one for each dataset ID that should be included in the view.\n", + "* `descriptors`: A descriptor instance, which is one of `{RealDescriptor, InorganicDescriptor, OrganicDescriptor, CategoricalDescriptor,` or `AlloyCompositionDescriptor}`.\n", + " * *Note 1*: Chemical formulas for the API take the key `\"formula\"`.\n", + " * *Note 2*: Properties take the key `\"Property [property name]\"`.\n", + " * *Note 3*: Strings are **Case-sensitive!**\n", + "* `roles`: A role for each descriptor, as a string, which is one of `{'input', 'output', 'latentVariable',` or `'ignored'}`." ] }, { @@ -115,16 +120,26 @@ "dv_builder = DataViewBuilder()\n", "dv_builder.dataset_ids(['172242']) # ID number for band gaps dataset\n", "\n", - "# Define descriptors\n", + "# Define crystallinity descriptor\n", "crystallinity = ['Single crystalline', 'Polycrystalline', 'Amorphous'] # Obtained from dataset\n", - "desc_crystal = CategoricalDescriptor(key='Property Crystallinity', categories=crystallinity)\n", - "dv_builder.add_descriptor(descriptor=desc_crystal, role='input')\n", + "desc_crystal = CategoricalDescriptor(key='Property Crystallinity', \n", + " categories=crystallinity)\n", + "dv_builder.add_descriptor(descriptor=desc_crystal, \n", + " role='input')\n", "\n", - "desc_formula = InorganicDescriptor(key='formula', threshold=1.0) # threshold <= 1.0; default in future releases\n", - "dv_builder.add_descriptor(descriptor=desc_formula, role='input')\n", + "# Define chemical formula descriptor\n", + "desc_formula = InorganicDescriptor(key='formula', \n", + " threshold=1.0)\n", + "dv_builder.add_descriptor(descriptor=desc_formula, \n", + " role='input')\n", "\n", - "desc_bandgap = RealDescriptor(key='Property Band gap', lower_bound=0.0, upper_bound=1e2, units='eV')\n", - "dv_builder.add_descriptor(descriptor=desc_bandgap, role='output')\n", + "# Define band gap descriptor\n", + "desc_bandgap = RealDescriptor(key='Property Band gap', \n", + " lower_bound=0.0, \n", + " upper_bound=1e3, \n", + " units='eV')\n", + "dv_builder.add_descriptor(descriptor=desc_bandgap, \n", + " role='output')\n", "\n", "# Build the configuration once all the pieces are in place\n", "view_config = dv_builder.build()" @@ -138,7 +153,7 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", - "After obtaining your customized configuration, you have to initialize a [`DataViewsClient`](http://citrineinformatics.github.io/python-citrination-client/modules/views/data_views_client.html) instance in order to create a data view from the configuration you built. The `create()` method returns the ID for the data view, which you will need for subsequent analysis and retraining." + "After obtaining your customized configuration, you have to initialize a [`DataViewsClient`](http://citrineinformatics.github.io/python-citrination-client/modules/views/data_views_client.html) instance in order to create a data view from the configuration you built." ] }, { @@ -148,14 +163,27 @@ "outputs": [], "source": [ "# Instantiate the base CitrinationClient\n", - "site = 'https://citrination.com' # site you want to access; we'll use the public site\n", - "client = CitrinationClient(api_key=os.environ.get('CITRINATION_API_KEY'), site=site)\n", + "site = 'https://citrination.com' # site you want to access; we'll use the public site\n", + "client = CitrinationClient(api_key=environ.get('CITRINATION_API_KEY'), \n", + " site=site)\n", "\n", "# Instantiate the DataViewsClient\n", "views_client = client.data_views\n", "views_client # reveal the methods" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `create()` method for the `DataViewClient` takes as input:\n", + "* `configuration`: A view configuration, like the template you created above.\n", + "* `name`: A name for the data view (must be unique among your data views).\n", + "* `description`: A description for the data view.\n", + "\n", + "and returns the ID for the data view, which you will need for subsequent analysis and retraining." + ] + }, { "cell_type": "code", "execution_count": null, @@ -163,11 +191,20 @@ "outputs": [], "source": [ "# Create a data view using the above configuration and store the ID\n", - "view_name = 'PyCC View ' + str(uuid.uuid4()) # random name to avoid clashes\n", + "view_name = 'PyCC View ' + str(uuid4())[:6] # random name to avoid clashes\n", "view_desc = 'This view was created by the PyCC API tutorial.'\n", - "view_id = views_client.create(configuration=view_config, name=view_name, description=view_desc)\n", + "view_id = views_client.create(configuration=view_config, \n", + " name=view_name, \n", + " description=view_desc)\n", "print('Data view {} was successfully created.'.format(view_id))\n", - "print('It can be accessed at {}/data_views/{}.'.format(site, view_id))" + "print('It can be accessed at {}/data_views/{}'.format(site, view_id))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Clicking the above URL will take you to the data view you just created on your deployment of Citrination." ] }, { @@ -198,7 +235,7 @@ "metadata": {}, "source": [ "### Check status of services\n", - "If there's a lot of data, training might take some time, and you might want to check when `predict` services are ready. Other possible services include `experimental_design`, `data_reports`, and `model_reports`." + "If there's a lot of data, training might take some time, and you might want to check when certain services are ready. Possible services enabled by data views include `predict`, `experimental_design`, `data_reports`, and `model_reports`." ] }, { @@ -207,13 +244,19 @@ "metadata": {}, "outputs": [], "source": [ - "# Use a loop to monitor status\n", + "# Use a loop to monitor view status\n", "while True:\n", - " predict_state = views_client.get_data_view_service_status(view_id).predict.reason\n", - " print(predict_state)\n", - " if predict_state == 'Predict services are ready.':\n", + " view_status = views_client.get_data_view_service_status(data_view_id=view_id)\n", + " \n", + " # Design and Predict are most important endpoints to check\n", + " if (view_status.experimental_design.ready and\n", + " view_status.predict.event.normalized_progress == 1.0):\n", + " print(\"Data view ready!\")\n", + " print(\"Data view URL: {}/data_views/{}\".format(site, view_id))\n", " break\n", - " time.sleep(10)" + " else:\n", + " print(\"Waiting for data view services...\")\n", + " sleep(10)" ] }, { @@ -271,7 +314,7 @@ "To recap, this notebook went through the steps for creating a data view using the API.\n", "1. First, we used the `DataViewBuilder` object to specify the configuration.\n", "2. Then, we trained the model, which is simple as long as the configuration is correct.\n", - "3. Lastly, we explored some of the post-processing capabilities, such as retraining and submitting predictions." + "3. We showed how to monitor the status of various endpoints enabled by data views." ] }, { diff --git a/citrination_api_examples/clients_sequence/3_models_client_api_tutorial.ipynb b/citrination_api_examples/clients_sequence/3_models_client_api_tutorial.ipynb index 0291c76..cdcc50d 100644 --- a/citrination_api_examples/clients_sequence/3_models_client_api_tutorial.ipynb +++ b/citrination_api_examples/clients_sequence/3_models_client_api_tutorial.ipynb @@ -15,8 +15,6 @@ "\n", "*Authors: Enze Chen, Eddie Kim*\n", "\n", - "**Note**: The [`ModelsClient`](http://citrineinformatics.github.io/python-citrination-client/modules/models/models_client.html) is now linked as an attribute of the [`DataViewsClient`](http://citrineinformatics.github.io/python-citrination-client/modules/views/data_views_client.html). Since this sub-client has many capabilties, this tutorial will still exist as a stanadalone reference.\n", - "\n", "In this notebook, we will cover how to use the `ModelsClient` to interface with *existing* data views and ML models through the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/). We will demonstrate how to analyze ML models and leverage them for prediction and design using the [Band gaps from Strehlow and Cook](https://citrination.com/datasets/1160/show_search?searchMatchOption=fuzzyMatch) dataset, where we will have created a model mapping:\n", "\n", "$$\\text{Chemical formula (inorganic) + Crystallinity (categorical)} \\longrightarrow \\boxed{\\text{ML model}} \\longrightarrow \\text{Band gap (real)}$$" @@ -82,15 +80,16 @@ "outputs": [], "source": [ "# Standard packages\n", - "import os\n", - "import time\n", - "import uuid # generating random IDs\n", + "from os import environ # get environment variables\n", + "from time import sleep # wait time\n", + "from uuid import uuid4 # generating random IDs\n", "\n", "# Third-party packages\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "\n", + "# Citrine packages\n", "from citrination_client import *\n", "from citrination_client.models.design import Target" ] @@ -103,13 +102,7 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", - "We will start by initializing the `ModelsClient` from the `CitrinationClient` and look at some basic properties of the view using `get_data_view()`. The returned `DataView` object has the following properties:\n", - "* `id`: The view ID.\n", - "* `name`: The name of the view.\n", - "* `description`: The description of the view.\n", - "* `datasets`: A list of datasets used in the view.\n", - "* `column_names`: A list of column names in the view.\n", - "* `columns`: A list of columns in the view (objects extend [`BaseColumn`](https://github.com/CitrineInformatics/python-citrination-client/tree/master/citrination_client/models/columns))." + "We will start by initializing the `ModelsClient` from the `CitrinationClient`." ] }, { @@ -119,14 +112,28 @@ "outputs": [], "source": [ "# Instantiate the base CitrinationClient\n", - "site = 'https://citrination.com' # site you want to access; we'll use the public site\n", - "client = CitrinationClient(api_key=os.environ.get('CITRINATION_API_KEY'), site=site)\n", + "site = 'https://citrination.com' # site you want to access; we'll use the public site\n", + "client = CitrinationClient(api_key=environ.get('CITRINATION_API_KEY'), \n", + " site=site)\n", "\n", "# Instantiate the ModelsClient\n", "models_client = client.models\n", "models_client # reveal some methods" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can look at some basic properties of the view using the `get_data_view()` method. The returned `DataView` object has the following properties:\n", + "* `id`: The view ID.\n", + "* `name`: The name of the view.\n", + "* `description`: The description of the view.\n", + "* `datasets`: A list of datasets used in the view.\n", + "* `column_names`: A list of column names in the view.\n", + "* `columns`: A list of columns in the view (objects extend [`BaseColumn`](https://github.com/CitrineInformatics/python-citrination-client/tree/master/citrination_client/models/columns))." + ] + }, { "cell_type": "code", "execution_count": null, @@ -134,12 +141,12 @@ "outputs": [], "source": [ "# Look up the data view ID and analyze the view\n", - "view_id = 7753 # Band gaps model with only 100 data points for faster demonstration\n", - "data_view = models_client.get_data_view(view_id)\n", + "view_id = 7753 # Band gaps model with only 100 data points for faster demonstration\n", + "data_view = models_client.get_data_view(data_view_id=view_id)\n", "print('Data view name: {}.'.format(data_view.name))\n", "print('Data view description: {}'.format(data_view.description))\n", "print('Names of included datasets: {}.'.format([data_view.datasets[i].name for i in range(len(data_view.datasets))]))\n", - "print('Data view URL: {}/data_views/{}.'.format(site, view_id))" + "print('Data view URL: {}/data_views/{}'.format(site, view_id))" ] }, { @@ -165,7 +172,7 @@ "metadata": {}, "source": [ "### Check status of services\n", - "You can check on the various services in your view, which includes `predict`, `experimental_design`, `data_reports`, `model_reports`, using `get_data_view_service_status()`. A `ServiceStatus` object has the following properties:\n", + "You can check on the various services in your view, which includes `predict`, `experimental_design`, `data_reports`, `model_reports`, using `get_data_view_service_status()`. A [`ServiceStatus`](https://github.com/CitrineInformatics/python-citrination-client/blob/master/citrination_client/models/service_status.py) object has the following properties:\n", "* `ready`: A Boolean indicating whether or not the service can be used.\n", "* `context`: A contextual description of the current status: `notice`, `success`, `error`.\n", "* `reason`: A full sentence explanation of the service's status.\n", @@ -179,13 +186,13 @@ "outputs": [], "source": [ "# Check status of services in a loop\n", - "time.sleep(5)\n", - "while (True):\n", - " view_status = models_client.get_data_view_service_status(view_id)\n", + "sleep(5)\n", + "while True:\n", + " view_status = models_client.get_data_view_service_status(data_view_id=view_id)\n", " model_report_progress = view_status.model_reports.event.normalized_progress\n", " print('Model reports are still being generated, progress: {0:.1f}%.'.format(100 * model_report_progress))\n", " if (model_report_progress < 0.99):\n", - " time.sleep(15)\n", + " sleep(15)\n", " else:\n", " print('Model reports generated!')\n", " break" @@ -218,13 +225,13 @@ "outputs": [], "source": [ "# Get the Tsne object\n", - "tsne = models_client.tsne(view_id)\n", + "tsne = models_client.tsne(data_view_id=view_id)\n", "\n", "# Get first output Property in dict_keys object\n", "projection_key = list(tsne.projections())[0]\n", "\n", "# Get the t-SNE projection from the key\n", - "projection = tsne.get_projection(projection_key)\n", + "projection = tsne.get_projection(key=projection_key)\n", "max_index, max_value = (np.argmax(projection.responses), max(projection.responses))\n", "print('Highest band gap material: \\t{0}.'.format(projection.tags[max_index]))\n", "print('It has projected coordinates: \\t({0:.3f}, {1:.3f}).'.format(\n", @@ -267,11 +274,12 @@ "candidates = [{'formula':'MgO'}, {'formula':'GaN'}]\n", "\n", "# Predict endpoint\n", - "prediction_results = models_client.predict(view_id, candidates)\n", + "prediction_results = models_client.predict(data_view_id=view_id, \n", + " candidates=candidates)\n", "target_prop = projection_key\n", "\n", "# Get predicted value for first candidate\n", - "prediction_value = prediction_results[0].get_value(target_prop)\n", + "prediction_value = prediction_results[0].get_value(key=target_prop)\n", "print('{0} has a predicted {1} value of {2:.3f} +/- {3:.3f}.'.format(\n", " prediction_results[0].get_value('formula').value,\n", " prediction_value.key,\n", @@ -287,10 +295,12 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", + "*Note*: In order to submit design runs on Public Citrination, you will need an Admin account.\n", + "\n", "Once ML models have been trained, you can generate a list of candidate materials designed to achieve your target objectives. We can submit a new experimental design run using `submit_design_run()`, which takes as inputs:\n", "* `data_view_id`: The view ID.\n", "* `num_candidates`: The number of candidates to return.\n", - "* `effort`: A value $\\le 30$ indicating how much resource (time) to allocate towards design.\n", + "* `effort`: A value $\\le 30$ indicating how much resource to allocate towards design.\n", "* `target`: A [`Target`](https://github.com/CitrineInformatics/python-citrination-client/blob/master/citrination_client/models/design/target.py) instance, which consists of the name of the output column and the objective (`Max` or `Min`).\n", "* `constraints`: A list of [design constraints](https://github.com/CitrineInformatics/python-citrination-client/tree/master/citrination_client/models/design/constraints) that extend the [`BaseConstraint`](https://github.com/CitrineInformatics/python-citrination-client/blob/master/citrination_client/models/design/constraints/base.py) class.\n", "* `sampler`: The name of the sampler to use as a string, either `Default` or `This view`.\n", @@ -304,7 +314,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Submit the design run and obtain design run uuid\n", + "# Submit the design run and obtain a design run uuid\n", "design_run = models_client.submit_design_run(\n", " data_view_id=view_id,\n", " num_candidates=10,\n", @@ -328,7 +338,7 @@ "* `status`: The status string of the process, which can be `Accepted`, `Finished`, or `Killed`.\n", "* `messages`: A list of messages representing the steps the process has already progressed through.\n", "\n", - "If a design run is taking too long, you can end it with `kill_design_run()`." + "If a design run is taking too long, you can end it with the `kill_design_run()` method." ] }, { @@ -338,19 +348,20 @@ "outputs": [], "source": [ "# Check status of design in a loop\n", - "design_running = True\n", - "while (design_running):\n", - " process_status = models_client.get_design_run_status(view_id, design_id)\n", + "while True:\n", + " process_status = models_client.get_design_run_status(data_view_id=view_id, \n", + " run_uuid=design_id)\n", " design_status = process_status.status\n", " design_progress = process_status.progress\n", " print('Design is running, progress: {0:.1f}%.'.format(design_progress))\n", " if (design_status != 'Finished'):\n", - " time.sleep(15)\n", + " sleep(15)\n", " else:\n", " print('Design complete!')\n", - " design_running = False\n", + " break\n", " \n", - "# models_client.kill_design_run(view_id, design_id)" + "# models_client.kill_design_run(data_view_id=view_id, \n", + "# run_uuid=design_id)" ] }, { @@ -371,7 +382,8 @@ "metadata": {}, "outputs": [], "source": [ - "design_results = models_client.get_design_run_results(view_id, design_id)\n", + "design_results = models_client.get_design_run_results(data_view_id=view_id, \n", + " run_uuid=design_id)\n", "best_material = design_results.best_materials[0]\n", "print('The best material is {0} with a predicted target value of {1}.'.format(\n", " best_material['descriptor_values']['formula'], \n", @@ -388,7 +400,8 @@ "\n", "To recap, this notebook demonstrated the functionalities enabled by the `ModelsClient`, which means you can use the API to:\n", "* Interface with an existing data view that already has ML configured.\n", - "* Query t-SNE and Predict endpoints for data visualization and making predictions on new materials.\n", + "* Query the t-SNE endpoint for data visualization.\n", + "* Make predictions on new materials.\n", "* Submit design runs and generate optimized material candidates." ] }, diff --git a/citrination_api_examples/clients_sequence/4_search_client_api_tutorial.ipynb b/citrination_api_examples/clients_sequence/4_search_client_api_tutorial.ipynb index 63025e9..8ae4e3e 100644 --- a/citrination_api_examples/clients_sequence/4_search_client_api_tutorial.ipynb +++ b/citrination_api_examples/clients_sequence/4_search_client_api_tutorial.ipynb @@ -105,9 +105,10 @@ "source": [ "# Initialize the base CitrinationClient\n", "site = \"https://citrination.com\" # site you want to access; we'll use the public site\n", - "client = CitrinationClient(api_key=os.environ.get('CITRINATION_API_KEY'), site=site)\n", + "client = CitrinationClient(api_key=os.environ.get('CITRINATION_API_KEY'), \n", + " site=site)\n", "\n", - "# Access the SearchClient as an attribute\n", + "# Access the SearchClient from the attribute\n", "search_client = client.search\n", "search_client # reveal the methods" ] @@ -129,12 +130,12 @@ "\n", "Before we discuss the specifics of each method, we'll provide a high-level discussion about the structure of [`Query`](https://github.com/CitrineInformatics/python-citrination-client/tree/64aab061500811fae4767491e5b069bb4a4af068/citrination_client/search/core/query) objects. There are two generic types of queries used by the `SearchClient`:\n", "\n", - "1. `ReturningQuery` objects that actually returns specific objects (e.g. PIFs, datasets).\n", + "1. `ReturningQuery` objects that actually return specific objects with data (e.g. PIFs, datasets).\n", " * These are inputs to the search methods listed above.\n", "\n", "\n", - "1. Other `Query` objects that just match for specific fields (e.g. datasets, formulas).\n", - " * There is approximately a `Query` object for each PIF object ([see here](http://citrineinformatics.github.io/python-citrination-client/modules/search/pif_query_core.html))." + "2. Other `Query` objects that just match for specific fields (e.g. datasets, formulas).\n", + " * Roughly speaking, there is a `Query` object corresponding to each PIF object ([see here](http://citrineinformatics.github.io/python-citrination-client/modules/search/pif_query_core.html))." ] }, { @@ -170,7 +171,9 @@ "source": [ "### `extract_as`\n", "\n", - "`extract_as` is a powerful keyword that facilitates the aggregation of data from multiple sources. It takes a `string` with the alias to save a field under, and is useful when different datasets use slightly different names to describe the same Property. It will return the PIF records and relevant field all under the same `extract_as` name. [See here](../tutorial_sequence/3_IntroQueries.ipynb) for an example and discussion." + "`extract_as` is a powerful keyword that facilitates the aggregation of data from multiple sources. It takes a `string` with the alias to save a field under, and is useful when different datasets use slightly different names to describe the same Property. \n", + "\n", + "It will return the PIF records and relevant field all under the same `extract_as` name. This flattens the data from the hierarchical PIF format to facilitate analysis. [See here](../tutorial_sequence/3_IntroQueries.ipynb) for an example and discussion." ] }, { @@ -199,11 +202,12 @@ "print(\"The dataset URL is: {}/datasets/{}\".format(site, dataset_id))\n", "\n", "system_query = PifSystemReturningQuery(\n", - " size=5,\n", + " size=500, # Returns the total number of matching hits without retrieving any data.\n", " query=DataQuery(\n", " dataset=DatasetQuery(\n", " id=Filter(\n", " equal=str(dataset_id)))))\n", + "\n", "search_result = search_client.pif_search(system_query)\n", "print(\"Found {} PIFs in dataset {}.\".format(search_result.total_num_hits, dataset_id))" ] @@ -225,6 +229,73 @@ "print(pif.dumps(search_result.hits[0].system, indent=4))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: Filter a range of values\n", + "Whereas the `.system` attribute above returned the entire PIF, we can use the `.extracted` attribute to return only the fields of interest specified in the query." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "system_query = PifSystemReturningQuery(\n", + " size=500,\n", + " query=DataQuery(\n", + " dataset=DatasetQuery(id=Filter(equal=dataset_id)),\n", + " system=PifSystemQuery(\n", + " chemical_formula=ChemicalFieldQuery(\n", + " extract_as='Chemical formula',\n", + " filter=ChemicalFilter(equal='?x?y')),\n", + " properties=PropertyQuery(\n", + " name=FieldQuery(\n", + " filter=Filter(equal='Band gap')),\n", + " value=FieldQuery(\n", + " filter=Filter(min=3.0, max=6.0),\n", + " extract_as='Band gap')))))\n", + " \n", + "\n", + "search_result = search_client.pif_search(system_query)\n", + "print(\"Found {} PIFs in dataset {}.\".format(search_result.total_num_hits, dataset_id))\n", + "print([x.extracted for x in search_result.hits][:2])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example: Logic\n", + "We can search for materials that `SHOULD` be oxides but `MUST NOT` have only 1 oxygen atom." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "query_size = 5\n", + "query_logical = PifSystemReturningQuery(\n", + " size=query_size,\n", + " query=DataQuery(\n", + " dataset=DatasetQuery(\n", + " id=Filter(equal=str(dataset_id))),\n", + " system=PifSystemQuery(\n", + " chemical_formula=ChemicalFieldQuery(\n", + " extract_as='formula',\n", + " filter=[ChemicalFilter(equal='?xOy', logic=\"SHOULD\"),\n", + " ChemicalFilter(equal='?xO1', logic=\"MUST_NOT\")]))))\n", + "\n", + "search_result = search_client.pif_search(query_logical)\n", + "print(\"{} total hits, the first {} of which are:\".format(search_result.total_num_hits, query_size))\n", + "for i in range(query_size):\n", + " print(pif.dumps(search_result.hits[i].extracted))" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -255,6 +326,7 @@ " chemical_formula=ChemicalFieldQuery(\n", " filter=ChemicalFilter(\n", " equal='As2S3')))))\n", + "\n", "search_result = search_client.dataset_search(dataset_query)\n", "print('{} datasets matched this query.'.format(search_result.total_num_hits))" ] @@ -274,7 +346,7 @@ "source": [ "first = search_result.hits[0]\n", "print('A matching dataset is \"{}\" with ID {}.\\nIt was made by {} at {}.'.format(\n", - " first.name, first.id, first.owner, first.updated_at, first.num_pifs))" + " first.name, first.id, first.owner, first.updated_at))" ] }, { @@ -328,9 +400,9 @@ "[Back to ToC](#Table-of-contents)\n", "\n", "Some other topics that might interest you include:\n", + "* Other examples on [learn-citrination](https://github.com/CitrineInformatics/learn-citrination), including [Intro](../tutorial_sequence/3_IntroQueries.ipynb) and [Advanced](../tutorial_sequence/AdvancedQueries.ipynb) queries.\n", "* [DataClient](http://citrineinformatics.github.io/python-citrination-client/tutorial/data_examples.html) - This allows you to create datasets and upload PIF data (only) using the API.\n", - " * There is also a corresponding [tutorial](1_data_client_api_tutorial.ipynb).\n", - "* Other examples on [learn-citrination](https://github.com/CitrineInformatics/learn-citrination), including [Intro](../tutorial_sequence/3_IntroQueries.ipynb) and [Advanced](../tutorial_sequence/AdvancedQueries.ipynb) queries." + " * There is also a corresponding [tutorial](1_data_client_api_tutorial.ipynb)." ] } ], diff --git a/citrination_api_examples/clients_sequence/5_sequential_learning_api_tutorial.ipynb b/citrination_api_examples/clients_sequence/5_sequential_learning_api_tutorial.ipynb index 6bb3f1f..072e107 100644 --- a/citrination_api_examples/clients_sequence/5_sequential_learning_api_tutorial.ipynb +++ b/citrination_api_examples/clients_sequence/5_sequential_learning_api_tutorial.ipynb @@ -17,6 +17,10 @@ "\n", "In this notebook, we will cover how to perform **sequential learning** (SL) using the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/). [Sequential learning](https://citrine.io/platform/sequential-learning/) is the key workflow which allows machine learning algorithms and in-lab experiments to iteratively inform each other.\n", "\n", + "**Objective**: We want to optimize the radius of CdSe nanoparticles to achieve a target bandgap.\n", + "\n", + "![Band gap graphic](https://raw.githubusercontent.com/CitrineInformatics/community-tools/master/templates/fig/bandgap_graphic.png)\n", + "\n", "To replace the need for an actual laboratory, this notebook uses a simple *toy function* that allows for \"measurements\" on the data.\n", "\n", "**NOTE**: If you want to run the sequential learning code in the final part of this tutorial on the public version of Citrination (https://citrination.com), then you will need an Admin account to run design jobs." @@ -81,17 +85,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Standard packages\n", - "import os\n", - "import uuid\n", + "from os import environ # get environment variables\n", + "from time import sleep # wait time\n", + "from uuid import uuid4 # generate random strings\n", "\n", "# Third-party packages\n", "from sequential_learning_wrappers import * # Helper functions to wrap several API endpoints together\n", - "%matplotlib inline" + "\n", + "# Magic settings\n", + "%matplotlib inline\n", + "%load_ext autoreload\n", + "%autoreload 2" ] }, { @@ -107,12 +116,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "site = \"https://citrination.com\" # site you want to access; we'll use the public site\n", - "client = CitrinationClient(api_key=os.environ.get(\"CITRINATION_API_KEY\"), site=site)" + "site = \"https://citrination.com\" # site you want to access; we'll use the public site\n", + "client = CitrinationClient(api_key=environ.get(\"CITRINATION_API_KEY\"), \n", + " site=site)" ] }, { @@ -123,38 +133,40 @@ "\n", "[Back to ToC](#Table-of-contents)\n", "\n", - "Since we aren't using a real laboratory, we need access to a quick way to generate \"correct\" measurements. A simple placeholder here is to use a function that sums the squares of its inputs. The goal, in this case, will be to find the global minimum located at the origin. \n", + "Since we aren't using a real laboratory, we need access to a quick way to generate \"correct\" measurements. \n", "\n", - "In a real example, we could minimize or maximize any output: compressive strengths, conductivities, and so on." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### `toy_func()`\n", - "This function takes a list of values and sums the squares of the values." + "In this example, we are modelling the band gap of ellipsoidal CdSe nanoparticles, where the band gap can be tuned by nanoparticle size (due to quantum confinement)." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "def toy_func(inputs):\n", - " return np.sum(np.square(inputs))" + "# A toy function to generate \"ground truth measurements\"\n", + "def bandgap_diff(radii, target=1.9, bulk_bg=1.76): # Close-to-actual values for CdSe nano band gaps\n", + " nano_bg = bulk_bg + sum(10/(r**2) for r in radii) # the last term here comes from particle-in-a-box-like energy\n", + " return abs(target - nano_bg) # get how close you are to the target" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best (lowest) value in initial training set: 0.1276917166095386\n" + ] + } + ], "source": [ - "# Generate random inputs and outputs\n", - "toy_x = [np.random.normal(loc=3.0, scale=1.0, size=(1, 2))[0] for x in range(20)]\n", - "toy_y = [toy_func(x) for x in toy_x]\n", + "# Generate random initial inputs and outputs\n", + "toy_x = [np.random.normal(loc=100.0, scale=30.0, size=(1, 2))[0] for x in range(16)]\n", + "toy_y = [bandgap_diff(x) for x in toy_x]\n", "\n", "initial_best_measured_value = min(toy_y)\n", "\n", @@ -165,72 +177,141 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "### Plot the data\n", "Now we can plot the initial training set, and color it by the function value." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# Plot initial training set, colored by toy function values\n", "plt.rcParams.update({'figure.figsize':(8, 6), 'font.size':18, 'lines.markersize':8})\n", "plt.scatter(np.array(toy_x)[:,0], np.array(toy_x)[:,1],\n", " c=toy_y, cmap=plt.cm.plasma)\n", - "plt.colorbar(label='toy function value')\n", + "plt.colorbar(label='Band gap absolute difference (eV)')\n", "plt.xlabel(r'$x_1$')\n", "plt.ylabel(r'$x_2$')\n", - "plt.xlim(-5,5)\n", - "plt.ylim(-5,5)\n", "plt.show()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save the dataset to a PIF" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\"toy_initial_dataset_017021.json\" file successfully created.\n" + ] + } + ], "source": [ "# Write a PIF JSON dataset file\n", - "random_string = str(uuid.uuid4())[:6]\n", + "random_string = str(uuid4())[:6]\n", "output_file = 'toy_initial_dataset_{}.json'.format(random_string)\n", - "write_dataset_from_func(toy_func, output_file, toy_x)" + "write_dataset_from_func(test_function=bandgap_diff,\n", + " filename=output_file,\n", + " input_vals=toy_x)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Upload the data to Citrination" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset created: 181513\n", + "Dataset URL: https://citrination.com/datasets/181513\n" + ] + } + ], "source": [ "# Make a dataset, upload to Citrination, return/print the ID\n", "dataset_name = output_file.split('.')[0]\n", "dataset_id = upload_data_and_get_id(\n", - " client,\n", - " dataset_name,\n", - " output_file,\n", - " create_new_version=True,\n", - ")\n", + " client=client,\n", + " dataset_name=dataset_name,\n", + " dataset_local_fpath=output_file,\n", + " create_new_version=True)\n", + "\n", "print(\"Dataset created: {}\".format(dataset_id))\n", "print(\"Dataset URL: {}/datasets/{}\".format(site, dataset_id))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Make a data view" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data view created: 10787\n", + "Data view URL: https://citrination.com/data_views/10787\n" + ] + } + ], "source": [ "# Make a data view on Citrination, return/print the ID\n", "view_name = 'toy_view_{}'.format(random_string)\n", - "view_id = build_view_and_get_id(client, dataset_id, \n", - " input_keys=[\"Property x1\", \"Property x2\"], output_keys=[\"Property y\"],\n", - " view_name=view_name, view_desc=\"toy test view\")\n", + "view_desc = 'toy test view'\n", + "input_keys = [\"Property x1\", \"Property x2\"]\n", + "target_property = 'Property Band gap difference'\n", + "\n", + "view_id = build_view_and_get_id(\n", + " client=client, \n", + " dataset_id=dataset_id, \n", + " input_keys=input_keys, \n", + " output_keys=[target_property],\n", + " view_name=view_name, \n", + " view_desc=view_desc)\n", "\n", "print(\"Data view created: {}\".format(view_id))\n", "print(\"Data view URL: {}/data_views/{}\".format(site, view_id))" @@ -245,7 +326,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -277,11 +358,87 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---STARTING SL ITERATION #1---\n", + "Design ready\n", + "Created design run with ID 4a5516d1-ec4c-4fc8-b73e-c288753f0029\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Finished\n", + "SL iter #1, best predicted (value, uncertainty) = ('0.1313', '0.0072')\n", + "\"design-4a5516d1-ec4c-4fc8-b73e-c288753f0029.json\" file successfully created.\n", + "Dataset updated: 10 candidates added.\n", + "New dataset contains 26 PIFs.\n", + "Design ready\n", + "\n", + "---STARTING SL ITERATION #2---\n", + "Design ready\n", + "Created design run with ID 0b8fe17a-ec86-4a48-a997-6d860edf6fac\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Finished\n", + "SL iter #2, best predicted (value, uncertainty) = ('0.1211', '0.0025')\n", + "\"design-0b8fe17a-ec86-4a48-a997-6d860edf6fac.json\" file successfully created.\n", + "Dataset updated: 10 candidates added.\n", + "New dataset contains 36 PIFs.\n", + "Design ready\n", + "\n", + "---STARTING SL ITERATION #3---\n", + "Design ready\n", + "Created design run with ID 4d8817d8-1332-4625-8945-472086c1e5b6\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Finished\n", + "SL iter #3, best predicted (value, uncertainty) = ('0.113', '0.017')\n", + "\"design-4d8817d8-1332-4625-8945-472086c1e5b6.json\" file successfully created.\n", + "Dataset updated: 10 candidates added.\n", + "New dataset contains 46 PIFs.\n", + "Design ready\n", + "\n", + "---STARTING SL ITERATION #4---\n", + "Design ready\n", + "Created design run with ID 8d9b069a-aa22-4ef5-a5c8-508539793ff1\n", + "Design run status: Accepted\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Finished\n", + "SL iter #4, best predicted (value, uncertainty) = ('0.072', '0.073')\n", + "\"design-8d9b069a-aa22-4ef5-a5c8-508539793ff1.json\" file successfully created.\n", + "Dataset updated: 10 candidates added.\n", + "New dataset contains 56 PIFs.\n", + "Design ready\n", + "\n", + "---STARTING SL ITERATION #5---\n", + "Design ready\n", + "Created design run with ID 39c78154-02d6-489d-8f66-d826ff04b746\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Processing\n", + "Design run status: Finished\n", + "SL iter #5, best predicted (value, uncertainty) = ('0.039', '0.015')\n", + "\"design-39c78154-02d6-489d-8f66-d826ff04b746.json\" file successfully created.\n", + "Dataset updated: 10 candidates added.\n", + "New dataset contains 66 PIFs.\n", + "Design ready\n", + "SL finished!\n", + "\n" + ] + } + ], "source": [ "best_sl_pred_vals, best_sl_measured_vals = run_sequential_learning(\n", " client=client,\n", @@ -291,12 +448,11 @@ " design_effort=10,\n", " wait_time=10,\n", " num_sl_iterations=5,\n", - " input_properties=[\"Property x1\", \"Property x2\"],\n", - " target=[\"Property y\", \"Min\"],\n", + " input_properties=input_keys,\n", + " target=[target_property, \"Min\"],\n", " print_output=True,\n", - " true_function=toy_func,\n", - " score_type=\"MLI\"\n", - ")" + " true_function=bandgap_diff,\n", + " score_type=\"MLI\")" ] }, { @@ -316,9 +472,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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VapF3bp1G44fPz7E3j0mTJgQfO211zby8fFpERYW1vj111+vYG/WWG5rHpOSkuTll1+uWL9+/YZ+fn7NAwICmjVu3LjBuHHjQgC6d+9eY+LEiZUB6tevH545LTfrtNHY2FjPJ554IjQsLKyxj49Pi3LlyjW9++67r925c6fNmVb79+/37tKlS82AgIBmZcqUad6hQ4faO3bssH0D6mQi0rJ79+41li1bVrpVq1b1/Pz8mpctW7ZZ7969q8fHx9vkBkeOHPF6+OGHq1WtWjXcx8enRfny5Zu2adOmzrx587Kddbdt27ZSkZGR14aEhDTx9vZuERoaGj5o0KCq58+fz/aY3bt3ryEiLU+cOOHVs2fPGkFBQU0DAwNbfPzxx0H169cPB5g4cWLlzJ9vaGjofznEW2+9FdK2bds6FSpUaOLt7d0iJCSkSdeuXa/ds2ePzc/X3prHzLJ//vnHt3379rVLly7dPCAgoFnnzp1rHjly5L8PNfJ6rV977bUKItIy5/MHSExMlLJlyza7/vrr6+b/FcnOkd1WhwA/GWOeFBHbnVtgC/CkIzcXkUZYE8YfjTHds5QfAj4E+gDfXuZhahljDmb02w6UyaPth0AKcJMx5nhGn++BXcAE4DZH4neXcqV9eLtHEx75Yn228qRUC8O/38wPT7TB21OXsyqVL/7l4cEf4ddREPWpbf354/B5Z4j8CBp3c318SimlCpzFYuHff//1BahYsWK2hY8PPvhgTV9fX8vgwYNPiQjVqlVLAXj33XeDR4wYUb1p06aXhg4derJ06dKW5cuXB44cOTLswIEDpT799NNjmY/x+uuvVxg9enS1evXqJb744ovHExISPKZMmVIpKCgoX8NZSUlJcvPNN9eJiooKaNu27flevXrF+vr6WrZv3+6/YMGCci+99NKZJ5544syFCxc8ly5dWva11147GhwcnAbQsmXLRLAmjtddd139kydP+vTq1SumUaNGiSdPnvSeMWNGhbZt2wb+/fffu+rWrZsC1rV9N998c/3o6Gif+++//0zDhg0TV69eHdCxY8e6SUlJBf4mc8eOHf49evSo06tXr5hevXrFrlq1KuD7778P9vDwYNasWf9NtduzZ49Pu3bt6p89e9b73nvvjW3ZsuWlS5cueURFRZX57bffAu69997zAKtXr/bv0qVL3YCAgPS+ffvGhIaG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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "plot_sl_results(best_sl_measured_vals, best_sl_pred_vals, initial_best_measured_value)" ] diff --git a/citrination_api_examples/clients_sequence/6_sequential_learning_steel_fatigue.ipynb b/citrination_api_examples/clients_sequence/6_sequential_learning_steel_fatigue.ipynb new file mode 100644 index 0000000..7d27112 --- /dev/null +++ b/citrination_api_examples/clients_sequence/6_sequential_learning_steel_fatigue.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![logo](https://github.com/CitrineInformatics/community-tools/blob/master/templates/fig/citrine_banner_2.png?raw=true)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sequential Learning Workshop\n", + "*Authors: Edward Kim, Enze Chen, Nils Persson*\n", + "\n", + "In this notebook, we will cover how to perform **sequential learning** (SL) using the [Citrination API](http://citrineinformatics.github.io/python-citrination-client/). [Sequential learning](https://citrine.io/platform/sequential-learning/) is the key workflow which allows machine learning algorithms and in-lab experiments to iteratively inform each other.\n", + "\n", + "To replace the need for an actual laboratory or simulation, this demo uses an existing dataset from the Open Citrination platform, with measurements of *steel fatigue strength across 437 experiments spanning 23 processing and formulation variables*. \n", + "\n", + "To simulate this experiment, we will redact the output measurement (Fatigue Strength) from all but 25 random experiments from the bottom quartile of performance. Each new experiment will be selected from the list of 412 other \"unmeasured\" points using the Citrination platform's design algorithm, with the goal of *maximizing Fatigue Strength*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Table of contents\n", + "1. [Setup](#1)\n", + "1. [Get Training Data](#2)\n", + "1. [Initial Measurements](#3)\n", + "1. [Run Sequential Learning](#4)\n", + " 1. [Design](#4.1)\n", + " 1. [Measure (and re-train)](#4.2)\n", + " 1. [Repeat](#4.3)\n", + "1. [Conclusion](#5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup\n", + "---\n", + "[Back to TOC](#toc)\n", + "\n", + "This notebook uses some convenience functions to wrap several API endpoints. These are contained in the file `sequential_learning_wrappers_class.py` and imported below. Review the docstrings and code in that file to learn more." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# IPython magic settings\n", + "%matplotlib inline\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "# Third-party packages\n", + "from steel_fatigue_wrapper_class import * # Helper functions to wrap several API endpoints together" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialize the CitrinationClient\n", + "\n", + "Initializing a `CitrinationClient` requires two arguments, `api_key` and `site`.\n", + "\n", + "If the following cell runs successfully, you will see `Client created successfully!`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize the CitrinationClient with your API key and deployment\n", + "site = \"https://citrination.com\"\n", + "client = CitrinationClient(api_key=os.environ.get('CITRINATION_API_KEY'),\n", + " site=site)\n", + "verify_client(client)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Get Training Data\n", + "---\n", + "[Back to TOC](#toc)\n", + "\n", + "Since we don't have access to an actual experiment or simulation, we will use data from an existing public dataset on steel fatigue." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.set_option('display.max_columns', 500)\n", + "orig_dataset_id = 150670 \n", + "df_steel = get_steel_dataset(client, orig_dataset_id)\n", + "ordered_cols = df_steel.columns.to_list()\n", + "print(\"{} entries spanning {} dimensions.\".format(df_steel.shape[0], df_steel.shape[1]-5))\n", + "df_steel.sample(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot histogram of Fatigue Strength values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.rcParams.update({'figure.figsize':(8, 7), 'font.size':14, 'lines.markersize':8})\n", + "df_steel['Fatigue Strength'].hist(bins=20)\n", + "plt.xlabel('Fatigue Strength (MPa)')\n", + "plt.ylabel('Number of Entries')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Analyze simple statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_steel['Fatigue Strength'].describe()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generate Training Set\n", + "\n", + "We will select 25 points from the bottom 25% of the dataset (in terms of Fatigue Strength) to simulate an initial experimental design space. We will have access to the Fatigue Strength of these 25 training points, but it will be redacted from the remaining 412. Thus, our initial model will be constructed on these below-average candidates.\n", + "\n", + "To \"measure\" a new candidate, we simply look up its Fatigue Strength from the original dataset. This process (the splitting and the \"measurement\") uses the functionality of our `SearchClient` under the hood." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set a cutoff value for Fatigue Strength\n", + "target_col = 'Fatigue Strength'\n", + "target_max = np.percentile(df_steel['Fatigue Strength'], 25) # 50th percentile of fatigue strength\n", + "\n", + "# Split and redact original dataset\n", + "all_pifs = split_dataset(client,\n", + " orig_dataset_id,\n", + " target_col,\n", + " target_max,\n", + " num_train=25)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Initial Measurements\n", + "---\n", + "[Back to TOC](#toc)\n", + "\n", + "We'll now write our initial training data to a JSON file and upload it to Citrination using our `client`. This involves creating a new Dataset, then defining a DataView to run predict and design services.\n", + "\n", + "**Start from here if you want to start-over an SL run**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "random_string = str(uuid4())[:6]\n", + "meas_dataset_name = \"SL_demo_dataset_{}\".format(random_string)\n", + "\n", + "# Write to file\n", + "if not os.path.exists('temp'):\n", + " os.makedirs('temp')\n", + "dataset_file = os.path.join(\"temp\", meas_dataset_name+\".json\")\n", + "with open(dataset_file, \"w\") as f:\n", + " f.write(pif.dumps(all_pifs, indent=4))\n", + "\n", + "# Upload to Citrination\n", + "dataset_id = upload_data_and_get_id(client,\n", + " meas_dataset_name,\n", + " dataset_file,\n", + " create_new_version=True)\n", + "\n", + "print(\"Dataset created: {}/datasets/{}\".format(site, dataset_id))\n", + "print('The name is \"{}.\"'.format(meas_dataset_name))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create a DataView\n", + "\n", + "We now create a DataView to model our initial training data and run design services. We will select the `chemical formula` and the processing variables as inputs, and set the `Fatigue Strength` as an output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "search_template_client = client.data_views.search_template_client\n", + "avail_cols = search_template_client.get_available_columns(dataset_id)\n", + "\n", + "excluded_cols = \\\n", + " [col for col in avail_cols if ('Area Proportion' in col\n", + " or 'Reduction Ratio' in col\n", + " or 'composition' in col\n", + " or 'Fatigue Strength' in col\n", + " or 'Sample Number' in col)]\n", + "\n", + "input_cols = [col for col in avail_cols if col not in excluded_cols]\n", + "\n", + "print('Inputs:\\n{}'.format(input_cols))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Make a data view on Citrination, return/print the ID\n", + "\n", + "view_name = \"SL_demo_view_{}\".format(random_string)\n", + "\n", + "view_id = build_view_and_get_id(client,\n", + " dataset_id,\n", + " view_name,\n", + " view_desc='DataView for SL demo for Fatigue Strength.',\n", + " input_keys=input_cols,\n", + " output_keys=['Property Fatigue Strength',\n", + " 'Property Sample Number'],\n", + " model_type='default')\n", + "\n", + "print(\"Data view created: {}/data_views/{}\".format(site, view_id))\n", + "print('The name is \"{}.\"'.format(view_name))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "While model training proceeds, we can explore the DataView we just created." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Run Sequential Learning\n", + "---\n", + "[Back to TOC](#toc)\n", + "\n", + "We're now ready to run sequential learning (SL). SL consists of three main phases:\n", + "\n", + "- **design**: generate new candidates to test in the lab (or *in silico*)\n", + "- **measure**: test those new candidates and add the results to your dataset\n", + "- **retrain**: re-train the machine learning model using the new measurements\n", + "- **repeat**\n", + "\n", + "That's really all there is to it! We will manage the entire sequential learning process through an object called an `SL_run`, which has methods (`.design()` and `.measure()`) to run each of these steps. Let's instantiate one of those right now." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "meas_cols = ordered_cols+['iter']\n", + "SLR = SL_run(client=client,\n", + " view_id=str(view_id),\n", + " dataset_id=str(dataset_id),\n", + " orig_dataset_id=orig_dataset_id,\n", + " all_dataset_cols=meas_cols,\n", + " target=[\"Property Fatigue Strength\", \"Max\"],\n", + " score_type=\"MLI\",\n", + " sampler='This view')\n", + "\n", + "SLR.measurements[meas_cols].head(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.1 Design" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "design_effort = 10 # An integer between 1 and 30\n", + "SLR.design(design_effort=design_effort)\n", + "cand_cols = input_cols + ['Property Fatigue Strength', \n", + " 'Uncertainty in Property Fatigue Strength', \n", + " 'citrine_score']\n", + "SLR.candidates[cand_cols]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2 Measure (and re-train)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SLR.measure()\n", + "SLR.measurements[meas_cols].tail(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.3 Repeat\n", + "---\n", + "[Back to TOC](#toc)\n", + "\n", + "From here on out, we can repeat this process as much as we want (and ultimately run until in converges to a specified tolerance). This basically consists of `.design()` and `.measure()` cycles. To wrap things up, we'll run a loop of a few more iterations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "repeat_iters = 6\n", + "for i in range(repeat_iters):\n", + " SLR.design(design_effort=design_effort)\n", + " SLR.measure()\n", + " SLR.plot_sl_results();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Inspect the new data in a DataFrame\n", + "SLR.measurements[meas_cols].tail(repeat_iters+3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "---\n", + "[Back to ToC](#toc)\n", + "\n", + "After running this demo, you should have a sense for the steps involved in a sequential learning cycle, namely that it consists of **design** and **measure** phases. Design is run on the Citrination platform by training a model to fit your existing data, and returns candidates to measure. Measure is the phase where you (the experimentalist or computationalist) go and run your experiment!\n", + "\n", + "A few key takeaways from this demo:\n", + "* Building a model on Citrination is as easy as defining inputs, outputs, and latent variables.\n", + "* Design runs return candidates based on predicted output *and* uncertainty.\n", + "* Well-calibrated prediction uncertainties are vital to this process." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/citrination_api_examples/clients_sequence/sequential_learning_wrappers.py b/citrination_api_examples/clients_sequence/sequential_learning_wrappers.py index 723e9d3..dabcb01 100644 --- a/citrination_api_examples/clients_sequence/sequential_learning_wrappers.py +++ b/citrination_api_examples/clients_sequence/sequential_learning_wrappers.py @@ -1,17 +1,20 @@ ''' Authors: Eddie Kim, Enze Chen -This file contains wrapper functions that are used in the sequential learning API tutorial notebook. Detailed docstrings with method fuctions and parameters are given below. +This file contains wrapper functions that are used in the sequential learning +API tutorial notebook. Detailed docstrings with method fuctions and parameters +are given below. ''' -import json -from collections import OrderedDict +# Standard packages +import os from time import sleep +# Third-party packages import numpy as np import matplotlib.pyplot as plt -from citrination_client import (CitrinationClient, DataQuery, DatasetQuery, - Filter, PifSystemReturningQuery, - RealDescriptor) + +# Citrine packages +from citrination_client import * from citrination_client.models.design import Target from citrination_client.views.data_view_builder import DataViewBuilder from pypif import pif @@ -19,16 +22,13 @@ def write_dataset_from_func(test_function, filename, input_vals): - '''Given a function, write a dataset evaluated on given input values - - :param test_function: Function for generating dataset - :type test_function: Callable[[np.ndarray], float] - :param filename: Name of file for saving CSV dataset - :type filename: str - :param input_vals: List of input values to eval function over - :type input_vals: np.ndarray - :return: Doesn't return anything - :rtype: None + ''' + Given a function, write a dataset evaluated on given input values. + + :param test_function: Function for generating dataset. + :param filename: Name of file as a string for saving CSV dataset. + :param input_vals: List of input values as numpy array to evaluate function over. + :return: None. ''' pif_systems = [] @@ -44,31 +44,30 @@ def write_dataset_from_func(test_function, filename, input_vals): system.properties.append(func_input) func_output = Property() - func_output.name = 'y' + func_output.name = 'Band gap difference' func_output.scalars = test_function(val_row) system.properties.append(func_output) pif_systems.append(system) - with open(filename, "w") as f: + if not os.path.exists('temp'): + os.makedirs('temp') + + with open(os.path.join('temp', filename), "w") as f: f.write(pif.dumps(pif_systems, indent=4)) + print('"{}" file successfully created.'.format(filename)) def upload_data_and_get_id(client, dataset_name, dataset_local_fpath, - create_new_version = False, given_dataset_id = None): - '''Uploads data to a new/given dataset and returns its ID - - :param client: Client API object to pass in - :type client: CitrinationClient - :param dataset_name: Name of dataset - :type dataset_name: str - :param dataset_local_fpath: Local data filepath - :type dataset_local_fpath: str - :param create_new_version: Whether or not to make a new version - :param create_new_version: bool - :param given_dataset_id: ID if using existing dataset, defaults to None - :param given_dataset_id: int - :return: ID of the dataset - :rtype: int + create_new_version = False, given_dataset_id = None): + ''' + Uploads data to a new/given dataset and returns its ID. + + :param client: CitrinationClient API object to pass in. + :param dataset_name: Name of dataset as a string. + :param dataset_local_fpath: Local data filepath as a string. + :param create_new_version: Boolean flag for whether or not to make a new version. + :param given_dataset_id: Integer ID if using existing dataset; default = None. + :return dataset_id: Integer ID of the dataset. ''' if given_dataset_id is None: @@ -79,46 +78,37 @@ def upload_data_and_get_id(client, dataset_name, dataset_local_fpath, if create_new_version: client.data.create_dataset_version(dataset_id) - client.data.upload(dataset_id, dataset_local_fpath) + client.data.upload(dataset_id, os.path.join('temp', dataset_local_fpath)) assert (client.data.matched_file_count(dataset_id) >= 1), "Upload failed." return dataset_id -def build_view_and_get_id(client, dataset_id, input_keys, output_keys, view_name, view_desc = "", - wait_time = 2, print_output = False): - '''Builds a new data view and returns the view ID - - :param client: Client object - :type client: CitrinationClient - :param dataset_id: Dataset to build view from - :type dataset_id: int - :param view_name: Name of the new view - :type view_name: str - :param input_keys: Input key names - :type input_keys: List[str] - :param output_keys: Output key names - :type output_keys: List[str] - :param view_desc: Description for the view, defaults to "" - :param view_desc: str, optional - :param wait_time: Wait time in seconds before polling API - :type wait_time: int - :param print_output: Whether or not to print outputs - :type print_output: bool - :return: ID of the view - :rtype: int +def build_view_and_get_id(client, dataset_id, input_keys, output_keys, view_name, + view_desc = '', wait_time = 2, print_output = False): + ''' + Builds a new data view and returns the view ID. + + :param client: CitrinationClient object. + :param dataset_id: Integer ID of the dataset to build data view from. + :param view_name: Name of the new data view as a string. + :param input_keys: List of string representing input key names. + :param output_keys: List of string representing output key names. + :param view_desc: String description for the data view. + :param wait_time: Wait time in seconds (int) before polling API. + :param print_output: Boolean flag for whether or not to print outputs. + :return dv_id: Integer ID of the data view. ''' dv_builder = DataViewBuilder() dv_builder.dataset_ids([str(dataset_id)]) - dv_builder.model_type('default') for key_name in input_keys: - desc_x = RealDescriptor(key=key_name, lower_bound=-1e6, upper_bound=1e6) - dv_builder.add_descriptor(desc_x, role='input') + desc_x = RealDescriptor(key=key_name, lower_bound=-1e3, upper_bound=1e3) + dv_builder.add_descriptor(descriptor=desc_x, role='input') for key_name in output_keys: - desc_y = RealDescriptor(key=key_name, lower_bound=-1e6, upper_bound=1e6) - dv_builder.add_descriptor(desc_y, role='output') + desc_y = RealDescriptor(key=key_name, lower_bound=0, upper_bound=1e2) + dv_builder.add_descriptor(descriptor=desc_y, role='output') dv_config = dv_builder.build() @@ -127,49 +117,33 @@ def build_view_and_get_id(client, dataset_id, input_keys, output_keys, view_name dv_id = client.data_views.create( configuration=dv_config, name=view_name, - description=view_desc - ) + description=view_desc) + return dv_id -def run_sequential_learning(client, view_id, dataset_id, - num_candidates_per_iter, - design_effort, wait_time, - num_sl_iterations, input_properties, - target, print_output, - true_function, - score_type): - '''Runs SL design - - :param client: Client object - :type client: CitrinationClient - :param view_id: View ID - :type view_id: int - :param dataset_id: Dataset ID - :type dataset_id: int - :param num_candidates_per_iter: Candidates in a batch - :type num_candidates_per_iter: int - :param design_effort: Effort from 1-30 - :type design_effort: int - :param wait_time: Wait time in seconds before polling API - :type wait_time: int - :param num_sl_iterations: SL iterations to run - :type num_sl_iterations: int - :param input_properties: Inputs - :type input_properties: List[str] - :param target: ("Output property", {"Min", "Max"}) - :type target: List[str] - :param print_output: Whether or not to print outputs - :type print_output: bool - :param true_function: Actual function for evaluating measured/true values - :type true_function: Callable[[np.ndarray], float] - :param score_type: MLI or MEI - :type score_type: str - :return: 2-tuple: list of predicted scores/uncertainties; list of measured scores/uncertainties - :rtype: Tuple[List[float], List[float]] +def run_sequential_learning(client, view_id, dataset_id, num_candidates_per_iter, + design_effort, wait_time, num_sl_iterations, + input_properties, target, print_output, true_function, + score_type): + ''' + Runs SL design. + + :param client: CitrinationClient object. + :param view_id: Integer ID for the data view. + :param dataset_id: Integer ID for the data set. + :param num_candidates_per_iter: Integer number of candidates in a batch. + :param design_effort: Integer from 1 to 30 representing design effort. + :param wait_time: Wait time in seconds (int) before polling API. + :param num_sl_iterations: Integer number of SL iterations to run. + :param input_properties: List of strings representing input property keys. + :param target: List of strings for target property key and optimization goal. + :param print_output: Boolean flag for whether or not to print outputs. + :param true_function: Actual function for evaluating measured/true values. + :param score_type: String for candidate selection strategy: 'MLI' or 'MEI'. + :return: 2-tuple: (List of floats for predicted scores/uncertainties, + List of floats for measured scores/uncertainties) ''' - - best_sl_pred_vals = [] best_sl_measured_vals = [] @@ -181,7 +155,7 @@ def run_sequential_learning(client, view_id, dataset_id, print("\n---STARTING SL ITERATION #{}---".format(i+1)) _wait_on_ingest(client, dataset_id, wait_time, print_output) - _wait_on_data_view(client, dataset_id, view_id, wait_time, print_output) + _wait_on_data_view(client, view_id, wait_time, print_output) # Submit a design run design_id = client.models.submit_design_run( @@ -189,9 +163,7 @@ def run_sequential_learning(client, view_id, dataset_id, num_candidates=num_candidates_per_iter, effort=design_effort, target=Target(*target), - constraints=[], - sampler="Default" - ).uuid + constraints=[]).uuid if print_output: print("Created design run with ID {}".format(design_id)) @@ -266,7 +238,7 @@ def run_sequential_learning(client, view_id, dataset_id, # Retrain model w/ wait times client.models.retrain(view_id) - _wait_on_data_view(client, dataset_id, view_id, wait_time, print_output) + _wait_on_data_view(client, view_id, wait_time, print_output) if print_output: print("SL finished!\n") @@ -275,7 +247,16 @@ def run_sequential_learning(client, view_id, dataset_id, def _wait_on_ingest(client, dataset_id, wait_time, print_output = True): - # Wait for ingest to finish + ''' + Utility function to check for data ingest completion. + + :param client: CitrinationClient API object. + :param dataset_id: Integer ID for the dataset to check. + :param wait_time: Wait time in seconds (int) before polling API again. + :param print_output: Boolean flag for whether to display status messages. + :return: None. + ''' + sleep(wait_time) while (client.data.get_ingest_status(dataset_id) != "Finished"): if print_output: @@ -283,37 +264,63 @@ def _wait_on_ingest(client, dataset_id, wait_time, print_output = True): sleep(wait_time) -def _wait_on_data_view(client, dataset_id, view_id, wait_time, print_output = True): - is_view_ready = False +def _wait_on_data_view(client, view_id, wait_time, print_output = True): + ''' + Utility function to check for data view creation completion. + + :param client: CitrinationClient API object. + :param view_id: Integer ID for the data view to check. + :param wait_time: Wait time in seconds (int) before polling API again. + :param print_output: Boolean flag for whether to display status messages. + :return: None. + ''' + sleep(wait_time) - while (not is_view_ready): + while True: sleep(wait_time) design_status = client.data_views.get_data_view_service_status(view_id) if (design_status.experimental_design.ready and - design_status.predict.event.normalized_progress == 1.0): - is_view_ready = True + design_status.predict.event.normalized_progress == 1.0): if print_output: - print("Design ready") + print("Design ready.") + break else: print("Waiting for design services...") + sleep(2) def _wait_on_design_run(client, design_id, view_id, wait_time, print_output = True): - design_processing = True + ''' + Utility function to check for design run completion. + + :param client: CitrinationClient API object. + :param design_id: Integer ID of the submitted design run. + :param view_id: Integer ID for the data view to check. + :param wait_time: Wait time in seconds (int) before polling API again. + :param print_output: Boolean flag for whether to display status messages. + :return: None. + ''' + sleep(wait_time) - while design_processing: + while True: status = client.models.get_design_run_status(view_id, design_id).status if print_output: - print("Design run status: {}".format(status)) - + print("Design run status: {}.".format(status)) if status != "Finished": sleep(wait_time) else: - design_processing = False - + break + def plot_sl_results(measured, predicted, init_best): -# plt.rcParams.update({'figure.figsize':(8, 6), 'font.size':18}) + ''' + Helper function to plot the SL results for each iteration. + + :param measured: True/measured values of a property (float). + :param predicted: Predicted values of a property (float). + :param init_best: The best value of the property from the training set (float). + :return: None. + ''' # Measured results plt.plot( diff --git a/citrination_api_examples/clients_sequence/steel_fatigue_wrapper_class.py b/citrination_api_examples/clients_sequence/steel_fatigue_wrapper_class.py new file mode 100644 index 0000000..777bee6 --- /dev/null +++ b/citrination_api_examples/clients_sequence/steel_fatigue_wrapper_class.py @@ -0,0 +1,706 @@ +''' +Authors: Eddie Kim, Enze Chen, Nils Persson +This file contains wrapper functions that are used in the sequential learning +API tutorial notebook. Detailed docstrings with method fuctions and parameters +are given below. +''' + +# Standard packages +import os +from uuid import uuid4 +from time import time, sleep + +# Third party packages +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + +# Citrine packages +from citrination_client import * +from citrination_client.models.design import Target +from citrination_client.views.data_view_builder import DataViewBuilder +from pypif import pif +from pypif.obj import * + + +def verify_client(client): + ''' + Verify that the Client is able to create datasets. + + :param client: An instance of CitrinationClient. + :return: None. + ''' + dataset_id = None + try: + dataset = client.data.create_dataset( + name='Test valid API key '+str(uuid4()), + description='This empty dataset was created to test the Client connection.') + dataset_id = dataset.id + except: + print("The Client could not connect.\nPlease double check the deployment name and API key.") + return + print("Client created successfully!") + client.data.delete_dataset(dataset_id=dataset_id) + return + + +def get_steel_dataset(client, orig_dataset_id): + ''' + Retrieve and format the steel fatigue dataset as a DataFrame. + + :param client: An instance of CitrinationClient. + :param orig_dataset_id: An int representing the dataset ID. + :return df_steel: A pandas DataFrame for the dataset. + ''' + dataset_json = client.data.get_dataset_files(orig_dataset_id)[0] + df = pd.read_json(dataset_json._url) + + # Create Composition and Property columns and combine into one DataFrame + df_form = df['composition'].apply(lambda dl: pd.Series({d['element']:d['actualWeightPercent']['value'] for d in dl})) + df_proc = df['properties'].apply(lambda dl: pd.Series({d['name']:d['scalars'][0]['value'] for d in dl})) + df_steel = pd.concat([df_form, df_proc], axis=1) + + return df_steel + + +def split_dataset(client, dataset_id, target_col, target_max, + num_train = 20, random_seed = 1): + ''' + Splits an existing dataset such that num_train entries with + target_col below target_max have their value of target_col + retained, while the rest have it redacted. + + :param client: Client API object to pass in. + :param dataset_id: An int representing the ID of dataset to split. + :param target_col: A string for column name to filter and split on. + :param target_max: Max float value of target_col to allow in training set. + :param num_train: An int for the number of training points to keep. + :param random_seed: A random seed (int) to fix things for testing. + :return all_pifs: A list of PIF Systems. + ''' + + # Get PIFs with Fatigue Strength above cutoff + system_query_high = PifSystemReturningQuery( + size=9999, + query=DataQuery( + dataset=DatasetQuery( + id=Filter( + equal=dataset_id)), + system=PifSystemQuery( + properties=PropertyQuery( + name=FieldQuery(filter=Filter(equal=target_col)), + value=FieldQuery(filter=Filter(min=target_max+1e-6)) + ) + ) + ) + ) + + query_result_high = client.search.pif_search(system_query_high) + print('Entries in top split:', query_result_high.total_num_hits) + + # Get PIFs with Fatigue Strength below cutoff + system_query_low = PifSystemReturningQuery( + size=9999, + query=DataQuery( + dataset=DatasetQuery( + id=Filter( + equal=dataset_id)), + system=PifSystemQuery( + properties=PropertyQuery( + name=FieldQuery(filter=Filter(equal=target_col)), + value=FieldQuery(filter=Filter(max=target_max)) + ) + ) + ) + ) + + query_result_low = client.search.pif_search(system_query_low) + print('Entries in bottom split:', query_result_low.total_num_hits) + + # Choose some number of the low values to use as training points + # np.random.seed(random_seed) + low_hits = query_result_low._hits + np.random.shuffle(low_hits) + low_hits_split = np.split(low_hits, [num_train]) + train_pifs = [h._system for h in low_hits_split[0]] + unmeasured_pifs = [h._system for h in low_hits_split[1]] \ + + [h._system for h in query_result_high._hits] + print("{} train PIFs and {} possible candidate PIFs.".format( + len(train_pifs), len(unmeasured_pifs))) + + # Redact the target_col values from the "unmeasured" candidates + for system in unmeasured_pifs: + fatigue = [p for p in system.properties if p._name == 'Fatigue Strength'][0] + fatigue.scalars[0]._value = None + + all_pifs = train_pifs + unmeasured_pifs + return all_pifs + + +def upload_data_and_get_id(client, dataset_name, dataset_local_fpath, + create_new_version = False, given_dataset_id = None): + ''' + Uploads data to a new/given dataset and returns its ID. + + :param client: CitrinationClient API object to pass in. + :param dataset_name: Name of dataset as a string. + :param dataset_local_fpath: Local data filepath as a string. + :param create_new_version: Boolean flag for whether or not to make a new version. + :param given_dataset_id: ID (int) if using existing dataset, defaults to None. + :return dataset_id: Integer ID of the dataset. + ''' + + if given_dataset_id is None: + dataset = client.data.create_dataset(dataset_name) + dataset_id = dataset.id + else: + dataset_id = given_dataset_id + if create_new_version: + client.data.create_dataset_version(dataset_id) + + # Guard against AWS timeout + start = time() + timeout = 240 + while time() - start < timeout: + sleep(1) + try: + print('Uploading data...') + client.data.upload(dataset_id, dataset_local_fpath) + break + except: + if time() - start >= timeout: + raise RuntimeError("Possible AWS timeout, try re-running.") + continue + + _wait_on_ingest(client, dataset_id, wait_time=15, print_output=True) + assert (client.data.matched_file_count(dataset_id) >= 1), "Upload failed." + return dataset_id + + +def build_view_and_get_id(client, dataset_id, view_name, input_keys, output_keys, + ignore_keys = [], view_desc = '', wait_time = 2, + print_output = False, model_type = 'default'): + ''' + Builds a new data view and returns the view ID. + + :param client: CitrinationClient object. + :param dataset_id: Integer ID of the dataset to build data view from. + :param view_name: Name of the new view as a string. + :param input_keys: Input key names as a list of strings. + :param output_keys: Output key names as a list of strings. + :param ignore_keys: Ignore (dummy) key names in this list of strings. + :param view_desc: String description for the view, defaults to ''. + :param wait_time: Wait time in seconds (int) before polling API. + :param print_output: Boolean flag for whether or not to print outputs. + :param model_type: 'default' or 'linear' ML model. + :return dv_id: Integer ID of the data view. + ''' + + dv_builder = DataViewBuilder() + dv_builder.dataset_ids([str(dataset_id)]) + dv_builder.model_type(model_type) + + for key_name in input_keys: + if 'formula' in key_name: + desc_x = InorganicDescriptor(key=key_name, + threshold=1) + dv_builder.add_descriptor(descriptor=desc_x, + role='input') + else: + desc_x = RealDescriptor(key=key_name, + lower_bound=-9999.0, + upper_bound=9999.0) + dv_builder.add_descriptor(desc_x, role='input') + + + for key_name in output_keys: + desc_y = RealDescriptor(key=key_name, + lower_bound=-9999.0, + upper_bound=9999.0) + dv_builder.add_descriptor(desc_y, role='output') + + for key_name in ignore_keys: + desc_i = RealDescriptor(key=key_name, + lower_bound=-9999.0, + upper_bound=9999.0) + dv_builder.add_descriptor(desc_i, role='ignore') + + dv_config = dv_builder.build() + + _wait_on_ingest(client, dataset_id, wait_time, print_output) + + dv_id = client.data_views.create( + configuration=dv_config, + name=view_name, + description=view_desc) + + return dv_id + + +def _wait_on_ingest(client, dataset_id, wait_time, print_output = True): + ''' + Utility function to check for data ingest completion. + + :param client: CitrinationClient API object. + :param dataset_id: Integer ID for the dataset to check. + :param wait_time: Wait time in seconds (int) before polling API again. + :param print_output: Boolean flag for whether to display status messages. + :return: None. + ''' + + sleep(wait_time) + while (client.data.get_ingest_status(dataset_id) != "Finished"): + if print_output: + print("Waiting for data ingest to complete...") + sleep(wait_time) + sleep(2) + + +def _wait_on_data_view(client, view_id, wait_time, print_output = True): + ''' + Utility function to check for data view creation completion. + + :param client: CitrinationClient API object. + :param view_id: Integer ID for the data view to check. + :param wait_time: Wait time in seconds (int) before polling API again. + :param print_output: Boolean flag for whether to display status messages. + :return: None. + ''' + + sleep(wait_time) + while True: + sleep(wait_time) + design_status = client.data_views.get_data_view_service_status(view_id) + if (design_status.experimental_design.ready and + design_status.predict.event.normalized_progress == 1.0): + if print_output: + print("Design ready.") + break + else: + print("Waiting for design services...") + sleep(2) + + +def candidates_to_df(candidates): + ''' + This function turns design candidates into a DataFrame. + + :param candidates: A list of materials candidates output from design runs. + :return df_cand: A pandas DataFrame of the candidates. + ''' + + df_cand = pd.DataFrame(candidates) + df_cand[list(df_cand['descriptor_values'].iloc[0].keys())] = \ + df_cand['descriptor_values'].apply(pd.Series) + df_cand = df_cand.drop(['descriptor_values', 'constraint_likelihoods'], axis=1) + for col in df_cand.columns: + try: + df_cand[col] = df_cand[col].astype(float) + except: + pass + return df_cand + + +def query_results_to_df(query_result): + ''' + This function puts query results from the SearchClient into a pandas DataFrame. + + :param query_result: Query results from the SearchClient. + :return df_query: A pandas DataFrame with data from the query results. + ''' + try: + query_result_list = query_result._hits + except: + query_result_list = query_result + + result_list = [{p._name:p._scalars[0]._value for p in h._system.properties} + for h in query_result_list] + formula_list = [{c._element:c._actual_weight_percent._value for c in h._system._composition} + for h in query_result_list] + full_results_dict = [{**d1, **d2} for d1,d2 in zip(formula_list, result_list)] + df_query = pd.DataFrame(full_results_dict).astype(float) + return df_query + + +class SL_run: + ''' + This class wraps the various steps of sequential learning to facilitate + running multiple SL iterations. + ''' + def __init__(self, client, view_id, dataset_id, orig_dataset_id, + all_dataset_cols, target, score_type, + design_effort = 25, wait_time = 10, + sampler = 'Default', print_output = True): + ''' + Constructor. + + :param client: CitinationClient object. + :param view_id: Integer ID of data view. + :param dataset_id: Integer ID of dataset. + :param orig_dataset_id: Integer ID of original dataset with all + measurements filled in. + :param all_dataset_cols: Full list of string column names expected for + measurements. + :param target: A list of string for the target property and optimization + objective ('Min' or 'Max'). + :param score_type: String for candidate selection strategy. 'MLI' or 'MEI' + :param wait_time: Wait time in seconds (int) before polling API. + :param sampler: What type of sampling to use for design. + ['Default' or 'This view'] + :param print_output: Boolean flag for whether or not to print outputs. + :return: An SL_run object. + ''' + + # Attributes from arguments + self.client = client + self.view_id = view_id + self.dataset_id = dataset_id + self.orig_dataset_id = orig_dataset_id + self.target = target + self.score_type = score_type + self.wait_time = wait_time + self.sampler = sampler + self.print_output = print_output + self.y_col = self.target[0].replace('Property ','') + + # Empty attributes to add onto later + self.curr_iter = 0 + self.curr_design_id = None + self.measurements = pd.DataFrame(columns=all_dataset_cols) + self.candidates = pd.DataFrame() + + # Dataset should have training data (iteration "0") + # Stick this in the measurements DataFrame as iter 0 + query_dataset = \ + PifSystemReturningQuery(size=9999, + query=DataQuery( + dataset=DatasetQuery( + id=Filter(equal=str(self.dataset_id))), + system=PifSystemQuery( + properties=PropertyQuery( + name=FieldQuery(filter=Filter(equal=self.y_col)), + value=FieldQuery(filter=Filter(exists=True)) + ) + ) + ) + ) + query_result = self.client.search.pif_search(query_dataset) + training_measurements = query_results_to_df(query_result) + training_measurements['iter'] = 0 + self.measurements = pd.concat([self.measurements, training_measurements], + sort=True) + self.measurements['iter'] = self.measurements['iter'].astype(int) + self.last_op = 'measure' + + + def _wait_on_ingest(self): + ''' + Utility function to check for data ingest completion. + + :return: None. + ''' + + sleep(self.wait_time) + while (self.client.data.get_ingest_status(self.dataset_id) != "Finished"): + if self.print_output: + print("Waiting for data ingest to complete...") + sleep(self.wait_time) + print("Ingest finished.") + sleep(2) + + + def _wait_on_data_view(self): + ''' + Utility function to check for data view creation completion. + + :return: None. + ''' + + sleep(self.wait_time) + while True: + design_status = self.client.data_views.get_data_view_service_status(self.view_id) + if (design_status.experimental_design.ready and + design_status.predict.event.normalized_progress == 1.0): + if self.print_output: + print("Design ready.") + break + else: + print("Waiting for design services...") + sleep(2) + + + def _wait_on_design_run(self, design_id): + ''' + Utility function to check for design run completion. + + :param design_id: Integer ID of the submitted design run. + :return: None. + ''' + + sleep(self.wait_time) + while True: + status = self.client.models.get_design_run_status(self.view_id, design_id).status + if self.print_output: + print("Design run status: {}.".format(status)) + if status != "Finished": + sleep(self.wait_time) + else: + break + sleep(2) + + + def _get_valid_candidates(self, num_candidates = 1, num_seeds = 20, + design_effort = 5): + ''' + Wrapper function for design runs that ensures we get back valid + candidates with non-zero uncertainty. + + :param num_candidates: The number of candidates to return. + :param num_seeds: The number of candidates to request from Design. + :param design_effort: The effort for Design runs. + :return valid_candidates: A list of material candidates from Design. + ''' + + valid_candidates = [] + while len(valid_candidates)1e-6] + valid_candidates.extend(candidates_filtered) + [vc.update(design_id=design_id) for vc in valid_candidates] + + if self.print_output: + print("{} candidates obtained.".format(num_candidates)) + + return valid_candidates + + + def design(self, num_candidates = 1, design_effort = 5): + ''' + Submit a design run, get candidates, and add them to self.candidates. + + :param num_candidates: Integer number of candidates to return for this run. + :param design_effort: Effort as an integer from 1 to 30. + :return: None. + ''' + + if self.last_op=='design': + raise Exception('Design was already run for this iteration') + self.curr_iter += 1 + + # Ensure ingest and view creation are complete + self._wait_on_ingest() + self._wait_on_data_view() + + # Get candidates (wrapper for submit_design_run) + candidates = self._get_valid_candidates(num_candidates=num_candidates, + design_effort=design_effort) + + # Candidate DataFrame + df_cand = candidates_to_df(candidates) + df_cand['iter'] = self.curr_iter + df_cand['design_effort'] = design_effort + df_cand = df_cand.sort_values('citrine_score', ascending=False).iloc[:num_candidates] + self.candidates = pd.concat([self.candidates, df_cand.copy()]) + + if self.print_output: + best_val_w_uncertainty = \ + df_cand[[self.target[0], + 'Uncertainty in '+self.target[0]]].iloc[0].values + print("SL iter #{}, best predicted (value, uncertainty) = {}".format( + self.curr_iter, best_val_w_uncertainty)) + + self.last_op = 'design' + + + def measure(self): + ''' + Measure the true_function for the most recent batch of candidates + and add them to self.measurements and the online dataset. + + :return: None. + ''' + + if self.last_op == 'measure': + raise Exception('Candidates were already measured for this iteration.') + if len(self.candidates) == 0: + raise Exception('No candidates to measure, please run design.') + + # Get candidates DataFrame for current iteration + curr_candidates_df = self.candidates.query("iter==@self.curr_iter") + search_results = [] + + # Search original dataset for the chosen samples + if self.print_output: + print("Measuring new candidates...") + for ii,cand in curr_candidates_df.iterrows(): + cand_prop_query = [PropertyQuery(name=FieldQuery(filter=Filter(equal='Sample Number')), + logic='MUST', + value=FieldQuery(filter= + Filter(min=cand['Property Sample Number']-0.1, + max=cand['Property Sample Number']+0.1)))] + system_query_cand = PifSystemReturningQuery( + size=9999, + query=DataQuery( + dataset=DatasetQuery( + id=Filter( + equal=self.orig_dataset_id)), + system=PifSystemQuery( + properties=cand_prop_query, + ) + ) + ) + query_result_cand = self.client.search.pif_search(system_query_cand) + search_results.append(query_result_cand.hits[0]) + + + # Store new measurements + curr_measurements = query_results_to_df(search_results) + curr_measurements['iter'] = self.curr_iter + self.measurements = pd.concat([self.measurements, + curr_measurements], + sort=True) + + + # Write measurements to dataset + self.curr_design_id = curr_candidates_df["design_id"].iloc[0] + temp_dataset_fpath = os.path.join('temp', + "design-{}.json".format(self.curr_design_id)) + + with open(temp_dataset_fpath, "w") as f: + f.write(pif.dumps([h._system for h in search_results], + indent=4)) + + + # Upload results and re-train model + upload_data_and_get_id( + self.client, + "", # No name needed for updating a dataset + temp_dataset_fpath, + given_dataset_id=self.dataset_id + ) + self._wait_on_ingest() + + if self.print_output: + print("Dataset updated: {} candidates added.".format(len(search_results))) + print("New dataset contains {} PIFs.".format(len(self.measurements))) + print('Retraining model...') + + # Re-train the model with the new data + self.client.data_views.models.retrain(self.view_id) + self._wait_on_data_view() + + self.last_op = 'measure' + + + def plot_sl_results(self, figsize = (8,7)): + ''' + Helper function to plot the SL results for each iteration. + + :param figsize: How large to make each rendered plot. + :return fig: A matplotlib figure object. + ''' + + # Get best point in initial training set + if self.target[1] == 'Min': + init_best = self.measurements.query("iter==0")[self.y_col].min() + else: + init_best = self.measurements.query("iter==0")[self.y_col].max() + + df_meas = self.measurements.reset_index().copy() + df_pred = self.candidates.reset_index().copy() + + # Data aggregation + if self.target[1]=='Min': + df_meas['best'] = df_meas[self.y_col].cummin() + df_best_cum = \ + df_meas.loc[df_meas.groupby('iter')['best'].idxmin()] + df_best_meas = \ + df_meas.loc[df_meas.groupby('iter')[self.y_col].idxmin()] + df_best_pred = \ + df_pred.loc[df_pred.groupby('iter')[self.target[0]].idxmin()] + else: + df_meas['best'] = df_meas[self.y_col].cummax() + df_best_cum = \ + df_meas.loc[df_meas.groupby('iter')['best'].idxmax()] + df_best_meas = \ + df_meas.loc[df_meas.groupby('iter')[self.y_col].idxmax()] + df_best_pred = \ + df_pred.loc[df_pred.groupby('iter')[self.target[0]].idxmax()] + + # Create Figure + fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True) + + # Cumulative Best Measurements + plt.sca(ax) + plt.plot('iter', + 'best', + data=df_best_cum, + color='xkcd:steel blue', + linewidth=3, + linestyle='-', + label="Best Measured Candidate (Cumulative)") + + # Best candidate in training set + plt.plot(np.arange(0, len(df_best_pred)+1), + [init_best] * (len(df_best_pred)+1), + color='xkcd:black', + linestyle='--', + linewidth=3, + label="Best Initial Point", + alpha=0.7) + + # Candidate Predictions with Error Bars per iteration + ax.errorbar(x='iter', + y=self.target[0], + fmt='o', + yerr="Uncertainty in "+self.target[0], + data=df_best_pred, + linewidth=3, + color="xkcd:orange", + label="Candidate Predictions w/ Uncertainty") + + # Candidate Measurements per iteration + plt.plot('iter', + self.y_col, + 's', + data=df_best_meas, + color='xkcd:maroon', + label="Candidate Measurements") + + plt.xlabel("SL iteration #") + plt.xticks(df_best_meas['iter']) + plt.ylabel("Fatigue Strength (MPa)") + plt.title("Optimizing using MLI") + plt.legend(loc='best') + plt.grid(b=False, axis='x') + plt.show() + + return fig diff --git a/citrination_api_examples/tutorial_sequence/1_ImportVASP.ipynb b/citrination_api_examples/tutorial_sequence/1_ImportVASP.ipynb index 356d927..feb3ddf 100644 --- a/citrination_api_examples/tutorial_sequence/1_ImportVASP.ipynb +++ b/citrination_api_examples/tutorial_sequence/1_ImportVASP.ipynb @@ -177,7 +177,8 @@ "outputs": [], "source": [ "site = 'https://citrination.com' # public site\n", - "client = CitrinationClient(api_key=os.environ['CITRINATION_API_KEY'], site=site)" + "client = CitrinationClient(api_key=os.environ['CITRINATION_API_KEY'], \n", + " site=site)" ] }, { @@ -224,7 +225,9 @@ "source": [ "# Comment this cell if you have an ID from a dataset you created via the website\n", "dataset_name = \"Tutorial dataset \" + str(uuid.uuid4())[:6]\n", - "dataset = client.data.create_dataset(name=dataset_name, description=\"Dataset for VASP tutorial.\", public=False)\n", + "dataset = client.data.create_dataset(name=dataset_name, \n", + " description=\"Dataset for VASP tutorial.\", \n", + " public=False)\n", "dataset_id = dataset.id\n", "print('Dataset created! {}/datasets/{}'.format(site, dataset_id))" ] diff --git a/citrination_api_examples/tutorial_sequence/2_WorkingWithPIFs.ipynb b/citrination_api_examples/tutorial_sequence/2_WorkingWithPIFs.ipynb index a53abd5..7783f7a 100644 --- a/citrination_api_examples/tutorial_sequence/2_WorkingWithPIFs.ipynb +++ b/citrination_api_examples/tutorial_sequence/2_WorkingWithPIFs.ipynb @@ -342,7 +342,7 @@ }, "outputs": [], "source": [ - "plt.rcParams.update({'font.size': 18, 'figure.figsize':(8, 6), 'lines.markersize':100})\n", + "plt.rcParams.update({'font.size': 18, 'figure.figsize':(8, 6), 'lines.markersize':10})\n", "plt.scatter(*zip(*points))\n", "plt.xlim(0, 1)\n", "plt.xlabel(\"Cu fraction\")\n", diff --git a/citrination_api_examples/tutorial_sequence/3_IntroQueries.ipynb b/citrination_api_examples/tutorial_sequence/3_IntroQueries.ipynb index c71d668..0998467 100644 --- a/citrination_api_examples/tutorial_sequence/3_IntroQueries.ipynb +++ b/citrination_api_examples/tutorial_sequence/3_IntroQueries.ipynb @@ -526,7 +526,8 @@ " filter=ChemicalFilter(equal='AlxCuy')),\n", " properties=PropertyQuery(\n", " name=FieldQuery(\n", - " filter=[Filter(equal=\"Formation energy\"), Filter(equal=\"Enthalpy of Formation\")]),\n", + " filter=[Filter(equal=\"Formation energy\"), \n", + " Filter(equal=\"Enthalpy of Formation\")]),\n", " value=FieldQuery(\n", " extract_as=\"formation_enthalpy\")))))\n", "\n", diff --git a/citrination_api_examples/tutorial_sequence/4_MLonCitrination.ipynb b/citrination_api_examples/tutorial_sequence/4_MLonCitrination.ipynb index fd3dd09..fe3d946 100644 --- a/citrination_api_examples/tutorial_sequence/4_MLonCitrination.ipynb +++ b/citrination_api_examples/tutorial_sequence/4_MLonCitrination.ipynb @@ -174,7 +174,7 @@ "output_type": "stream", "text": [ "We found 500 records.\n", - "[{'density': ['2.849276907145639'], 'formula': 'LiFeSiO4'}, {'density': ['2.6366293129465217'], 'formula': 'K3NiO2'}]\n" + "[{'density': ['4.032147167706144'], 'formula': 'Li2ZrO3'}, {'density': ['5.067462110078542'], 'formula': 'KSr2Cd2Sb3'}]\n" ] } ], @@ -199,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "slideshow": { "slide_type": "fragment" @@ -270,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": { "slideshow": { "slide_type": "fragment" @@ -310,13 +310,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "slideshow": { "slide_type": "fragment" } }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "We found 5000 records.\n", + "[{'density': ['4.032147167706144'], 'formula': 'Li2ZrO3'}, {'density': ['5.067462110078542'], 'formula': 'KSr2Cd2Sb3'}]\n" + ] + } + ], "source": [ "dataset_id = 150675\n", "query_size = 5000\n", @@ -352,7 +361,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": { "slideshow": { "slide_type": "fragment" @@ -363,7 +372,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "We predict the density of AlCu to be 8.1988 +/- 1.2069.\n" + "We predict the density of AlCu to be 8.0285 +/- 1.2208.\n" ] } ], @@ -427,14 +436,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Highest density compound is CuAu3Al96 with rho = 14.1870 +/- 0.9619.\n" + "Highest density compound is PtRe2RhAl96 with rho = 14.3593 +/- 1.3423.\n" ] } ], @@ -451,6 +460,14 @@ " print(\"Highest density compound is {0} with rho = {1:.4f} +/- {2:.4f}.\".format(\n", " best['formula'], best['value'], best['loss']))" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "This concludes the tutorial sequence on working with DFT data." + ] } ], "metadata": {