diff --git a/docs/source/workflows/design_spaces.rst b/docs/source/workflows/design_spaces.rst index b2bbd1174..52efb6ab0 100644 --- a/docs/source/workflows/design_spaces.rst +++ b/docs/source/workflows/design_spaces.rst @@ -419,25 +419,3 @@ We will require that formulations contain 2 ingredients, that no more than 1 sol ) registered_design_space = project.design_spaces.register(design_space) - -Sampling from Design Spaces ---------------------------- - -To sample candidates from a registered design space, follow the example below: - -.. code:: python - - from citrine import Citrine - from citrine.jobs.waiting import wait_while_executing - from citrine.informatics.design_spaces.sample_design_space import SampleDesignSpaceInput - - sample_input = SampleDesignSpaceInput(n_candidates=50) - sample_design_space_collection = design_space.sample_design_space_executions - sample_design_space_execution = sample_design_space_collection.trigger(sample_input) - - execution = wait_while_executing( - collection=sample_design_space_collection, execution=sample_design_space_execution - ) - - sampled_candidates = [candidate.material for candidate in execution.results()] - root_materials = [candidate.root for candidate in sampled_candidates] diff --git a/docs/source/workflows/generative_design.rst b/docs/source/workflows/generative_design.rst deleted file mode 100644 index a25e16863..000000000 --- a/docs/source/workflows/generative_design.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. _generative_design: - -[ALPHA] Generative Design Execution -=================================== -The Citrine Platform offers a Generative Design Execution tool that allows the creation of new molecules by applying mutations to a set of given seed molecules. -To use this feature, you need to provide a set of starting molecules and filtering parameters using the :class:`~citrine.informatics.generative_design.GenerativeDesignInput` class. - -The class requires you to define the seed molecules for generating mutations, the fingerprint type used to calculate the `fingerprint similarity `_, the minimum fingerprint similarity between the seed and mutated molecule, the number of initial mutations attempted per seed, and the minimum substructure counts for each mutated molecule. - -Various fingerprint types are available on the Citrine Platform, including Atom Pairs (AP), Path-Length Connectivity (PHCO), Binary Path (BPF), Paths of Atoms of Heteroatoms (PATH), Extended Connectivity Fingerprint with radius 4 (ECFP4) and radius 6 (ECFP6), and Focused Connectivity Fingerprint with radius 4 (FCFP4) and radius 6 (FCFP6). -Each fingerprint type captures different aspects of molecular structure and influences the generated mutations. -You can access these fingerprint types through the :class:`~citrine.informatics.generative_design.FingerprintType` enum, like `FingerprintType.ECFP4`. - -The `structure_exclusions` parameter allows you to control the structural features of mutated molecules. -It is a sequence of exclusion types corresponding to the types of structural features or elements to exclude from the list of possible mutation steps during the generative design process. -If a type is present in the sequence, the mutation steps generated by the process will avoid using that feature or element. -The available structure exclusion options can be found in the :class:`~citrine.informatics.generative_design.StructureExclusion` class. - -The `min_substructure_counts` parameter is a dictionary for constraining which substructures (represented by SMARTS strings, not SMILES strings) must appear in each mutated molecule, along with integer values representing the minimum number of times each substructure must appear in a molecule to be considered a valid mutation. `SMARTS `_ are necessary here because they are designed to more precisely identify molecular sub-structures in ways that SMILES cannot (SMILES are intended for whole molecules, not substructures). - -After the generative design process is complete, the mutations are filtered based on their similarity to the starting seed molecules. -Mutations that do not meet the similarity threshold or are duplicates will be discarded. The remaining mutations are returned as a subset of the original mutations in the form of a list of :class:`~citrine.informatics.generative_design.GenerativeDesignResult` objects. -These results contain information about the seed molecule, the mutation, the similarity score, and the fingerprint type used during execution. - -After triggering the execution and waiting for completion, the user can retrieve the results and utilize them in their work. -The following example demonstrates how to run a generative design execution on the Citrine Platform: - -.. code-block:: python - - from citrine.jobs.waiting import wait_while_executing - from citrine.informatics.generative_design import GenerativeDesignInput, FingerprintType, StructureExclusion - - # Trigger a new generative design execution - generative_design_input = GenerativeDesignInput( - seeds=["CCCCCCCCCCCCCCCCCC(=O)O", "CCCCCCCC\C=C/CCCCCCCC(O)=O"], # stearic acid and oleic acid - fingerprint_type=FingerprintType.ECFP4, - min_fingerprint_similarity=0.1, - mutation_per_seed=1000, - structure_exclusions=[ - StructureExclusion.BROMINE, - StructureExclusion.CHLORINE, - StructureExclusion.TRIPLE_BONDS, - StructureExclusion.IONS, - ], - min_substructure_counts={"[#8X2H]-[#6](=[#8])": 1} # enforce that a carboxyl group must be present - ) - generative_design_execution = project.generative_design_executions.trigger( - generative_design_input - ) - execution = wait_while_executing( - collection=project.generative_design_executions, execution=generative_design_execution, print_status_info=True - ) - generated = execution.results() - mutations = [(gen.seed, gen.mutated) for gen in generated] - - # Or get a completed execution by ID - execution_id = execution.uid - execution = project.generative_design_executions.get(execution_id) - generated = execution.results() - mutations = [(gen.seed, gen.mutated) for gen in generated] diff --git a/docs/source/workflows/index.rst b/docs/source/workflows/index.rst index e09214327..be02a5944 100644 --- a/docs/source/workflows/index.rst +++ b/docs/source/workflows/index.rst @@ -16,5 +16,4 @@ AI Engine data_sources predictor_reports predictor_evaluations - generative_design code_examples diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index 6c235c596..f5752882d 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "5.2.0" +__version__ = "5.3.0" diff --git a/src/citrine/exceptions.py b/src/citrine/exceptions.py index a8ee8b987..672687dbd 100644 --- a/src/citrine/exceptions.py +++ b/src/citrine/exceptions.py @@ -138,6 +138,13 @@ class WorkflowNotReadyException(RetryableException): """The workflow is not ready to be executed. I.e., still validating. (http status 425).""" +class FeatureRemovedError(NonRetryableException, AttributeError): + """The feature has been removed from the Citrine Platform. + + It is also an AttributeError, so hasattr() and introspection skip the removed accessors. + """ + + class PollingTimeoutError(NonRetryableException): """Polling for an asynchronous result has exceeded the timeout.""" diff --git a/src/citrine/informatics/design_candidate.py b/src/citrine/informatics/design_candidate.py index f747ac68e..d93386901 100644 --- a/src/citrine/informatics/design_candidate.py +++ b/src/citrine/informatics/design_candidate.py @@ -12,7 +12,6 @@ "MeanAndStd", "Mixture", "MolecularStructure", - "SampleSearchSpaceResultCandidate", "TopCategories", ] @@ -204,17 +203,3 @@ class HierarchicalDesignCandidate(Serializable["HierarchicalDesignCandidate"]): """:int: rank by score amongst other candidates produced by the design workflow""" material = properties.Object(HierarchicalDesignMaterial, "material") """:HierarchicalDesignMaterial: the material returned by the design workflow""" - - -class SampleSearchSpaceResultCandidate(Serializable["SampleSearchSpaceResultCandidate"]): - """A hierarchical candidate material generated by the Citrine Platform. - - This class represents the candidate computed by a design execution. - """ - - uid = properties.UUID("id") - """:UUID: unique external Citrine ID of the material""" - execution_uid = properties.UUID("id") - """:UUID: unique external Citrine ID of the execution""" - material = properties.Object(HierarchicalDesignMaterial, "material") - """:HierarchicalDesignMaterial: the material returned by the design workflow""" diff --git a/src/citrine/informatics/design_spaces/sample_design_space.py b/src/citrine/informatics/design_spaces/sample_design_space.py index e7832bbac..5c64576ca 100644 --- a/src/citrine/informatics/design_spaces/sample_design_space.py +++ b/src/citrine/informatics/design_spaces/sample_design_space.py @@ -1,18 +1,12 @@ -from citrine._serialization import properties -from citrine._serialization.serializable import Serializable +"""Design space sampling has been removed; this name remains so existing scripts fail clearly.""" +from citrine.exceptions import FeatureRemovedError -class SampleDesignSpaceInput(Serializable["SampleDesignSpaceInput"]): - """A Citrine Sample Design Space Execution Input. +SAMPLING_REMOVED = "Design space sampling has been removed from the Citrine Platform." - Parameters - ---------- - n_candidates: int - The number of non-correlated samples to draw from the design space domain. - """ +class SampleDesignSpaceInput: + """Removed. Constructing one raises :class:`~citrine.exceptions.FeatureRemovedError`.""" - n_candidates = properties.Integer("n_candidates") - - def __init__(self, *, n_candidates: int): - self.n_candidates: int = n_candidates + def __init__(self, *args, **kwargs): + raise FeatureRemovedError(SAMPLING_REMOVED) diff --git a/src/citrine/informatics/design_spaces/top_level_design_space.py b/src/citrine/informatics/design_spaces/top_level_design_space.py index 8343554fc..a883aa5b0 100644 --- a/src/citrine/informatics/design_spaces/top_level_design_space.py +++ b/src/citrine/informatics/design_spaces/top_level_design_space.py @@ -7,7 +7,8 @@ from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable from citrine._session import Session -from citrine.resources.sample_design_space_execution import SampleDesignSpaceExecutionCollection +from citrine.exceptions import FeatureRemovedError +from citrine.informatics.design_spaces.sample_design_space import SAMPLING_REMOVED __all__ = ["TopLevelDesignSpace"] @@ -72,7 +73,5 @@ def is_locked(self) -> bool: @property def sample_design_space_executions(self): - """Start a Sample Design Space Execution using the current Design Space.""" - return SampleDesignSpaceExecutionCollection( - project_id=self._project_id, design_space_id=self.uid, session=self._session - ) + """Removed. Accessing it raises :class:`~citrine.exceptions.FeatureRemovedError`.""" + raise FeatureRemovedError(SAMPLING_REMOVED) diff --git a/src/citrine/informatics/executions/__init__.py b/src/citrine/informatics/executions/__init__.py index 29d83ef89..b6f4c26d6 100644 --- a/src/citrine/informatics/executions/__init__.py +++ b/src/citrine/informatics/executions/__init__.py @@ -1,4 +1,3 @@ # flake8: noqa from .predictor_evaluation import * from .design_execution import * -from .generative_design_execution import * diff --git a/src/citrine/informatics/executions/generative_design_execution.py b/src/citrine/informatics/executions/generative_design_execution.py deleted file mode 100644 index 086f46769..000000000 --- a/src/citrine/informatics/executions/generative_design_execution.py +++ /dev/null @@ -1,42 +0,0 @@ -from collections.abc import Iterable -from functools import partial -from uuid import UUID - -from citrine._rest.resource import Resource -from citrine._utils.functions import format_escaped_url -from citrine.informatics.executions.execution import Execution -from citrine.informatics.generative_design import GenerativeDesignResult - - -class GenerativeDesignExecution(Resource["GenerativeDesignExecution"], Execution): - """The execution of a Generative Design task. - - Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. - Design executions also have a ``status_description`` field with more information. - - """ - - def _path(self): - return format_escaped_url( - "/projects/{project_id}/generative-design/executions/", project_id=self.project_id - ) - - @classmethod - def _build_results(cls, subset_collection: Iterable[dict]) -> Iterable[GenerativeDesignResult]: - for generation_result in subset_collection: - yield GenerativeDesignResult.build(generation_result) - - def results(self, *, per_page: int = 100) -> Iterable[GenerativeDesignResult]: - """Fetch the Generative Design Results for the particular execution, paginated.""" - path = self._path() + f"{self.uid}/results" - fetcher = partial(self._fetch_page, path=path, fetch_func=self._session.get_resource) - return self._paginator.paginate( - page_fetcher=fetcher, collection_builder=self._build_results, per_page=per_page - ) - - def result(self, *, result_id: UUID) -> GenerativeDesignResult: - """Fetch a Generative Design Result for the particular UID.""" - path = self._path() + f"{self.uid}/results/{result_id}" - data = self._session.get_resource(path, version=self._api_version) - result = GenerativeDesignResult.build(data) - return result diff --git a/src/citrine/informatics/executions/sample_design_space_execution.py b/src/citrine/informatics/executions/sample_design_space_execution.py deleted file mode 100644 index 298050481..000000000 --- a/src/citrine/informatics/executions/sample_design_space_execution.py +++ /dev/null @@ -1,62 +0,0 @@ -from collections.abc import Iterable -from functools import partial -from uuid import UUID - -from citrine._rest.resource import Resource -from citrine._utils.functions import format_escaped_url -from citrine.informatics.design_candidate import SampleSearchSpaceResultCandidate -from citrine.informatics.executions.execution import Execution - - -class SampleDesignSpaceExecution(Resource["SampleDesignSpaceExecution"], Execution): - """The execution of a Sample Design Space task. - - Possible statuses are INPROGRESS, SUCCEEDED, and FAILED. - Additional fields ``status_description`` and ``status_detail`` provide more information. - - """ - - _api_version = "v3" - design_space_id: UUID | None = None - - def _path(self): - return format_escaped_url( - "/projects/{project_id}/design-spaces/{design_space_id}/sample/", - project_id=self.project_id, - design_space_id=self.design_space_id, - ) - - @classmethod - def _pre_build(cls, data: dict) -> dict: - """Run data modification before building.""" - # Flatten the status object in order to match other workflow objects. - return { - **data, - "status_description": data["status"]["minor"], - "status_detail": data["status"]["detail"], - "status": data["status"]["major"], - } - - @classmethod - def _build_results( - cls, subset_collection: Iterable[dict] - ) -> Iterable[SampleSearchSpaceResultCandidate]: - for sample_result in subset_collection: - yield SampleSearchSpaceResultCandidate.build(sample_result) - - def results( - self, *, page: int | None = None, per_page: int = 100 - ) -> Iterable[SampleSearchSpaceResultCandidate]: - """Fetch the Sample Design Space Results for the particular execution, paginated.""" - path = self._path() + f"{self.uid}/results" - fetcher = partial(self._fetch_page, path=path, fetch_func=self._session.get_resource) - return self._paginator.paginate( - page_fetcher=fetcher, collection_builder=self._build_results, per_page=per_page - ) - - def result(self, *, result_id: UUID) -> SampleSearchSpaceResultCandidate: - """Fetch a Sample Design Space Result for the particular UID.""" - path = self._path() + f"{self.uid}/results/{result_id}" - data = self._session.get_resource(path, version=self._api_version) - result = SampleSearchSpaceResultCandidate.build(data) - return result diff --git a/src/citrine/informatics/generative_design.py b/src/citrine/informatics/generative_design.py index af42ffe5c..0a13918e5 100644 --- a/src/citrine/informatics/generative_design.py +++ b/src/citrine/informatics/generative_design.py @@ -1,21 +1,21 @@ +"""Generative design has been removed; these names remain so existing scripts fail clearly.""" + from gemd.enumeration.base_enumeration import BaseEnumeration -from citrine._serialization import properties -from citrine._serialization.serializable import Serializable +from citrine.exceptions import FeatureRemovedError +GENERATIVE_DESIGN_REMOVED = "Generative design has been removed from the Citrine Platform." + + +class GenerativeDesignInput: + """Removed. Constructing one raises :class:`~citrine.exceptions.FeatureRemovedError`.""" + + def __init__(self, *args, **kwargs): + raise FeatureRemovedError(GENERATIVE_DESIGN_REMOVED) -class FingerprintType(BaseEnumeration): - """[ALPHA] Fingerprint type used in Generative Design. - * AP is the Atom Pairs Fingerprint. - * PHCO is the Path-Length Connectivity Fingerprint. - * BPF is the Binary Path Fingerprint. - * PATH is the Paths of Atoms of Heteroatoms Fingerprint. - * ECFP4 is the Extended Connectivity Fingerprint with radius 4. - * ECFP6 is the Extended Connectivity Fingerprint with radius 6. - * FCFP4 is the Focused Connectivity Fingerprint with radius 4. - * FCFP6 is the Focused Connectivity Fingerprint with radius 6. - """ +class FingerprintType(BaseEnumeration): + """Removed along with generative design; kept so scripts reach the removal error.""" AP = "AP" PHCO = "PHCO" @@ -28,23 +28,7 @@ class FingerprintType(BaseEnumeration): class StructureExclusion(BaseEnumeration): - """[ALPHA] Structure exclusion type used in Generative Design. - - * `DOUBLE_BONDS` excludes mutation steps that add double bonds. - * `TRIPLE_BONDS` excludes mutation steps that add triple bonds. - * `ANIONS` excludes mutation steps that add anions. - * `CATIONS` excludes mutation steps that add cations. - * `IONS` excludes mutation steps that add anions and cations. - * `BORON` excludes mutation steps that add boron atoms. - * `PHOSPHORUS` excludes mutation steps that add phosphorus atoms. - * `SULFUR` excludes mutation steps that add sulfur atoms. - * `NITROGEN` excludes mutation steps that add nitrogen atoms. - * `OXYGEN` excludes mutation steps that add oxygen atoms. - * `FLUORINE` excludes mutation steps that add fluorine atoms. - * `BROMINE` excludes mutation steps that add bromine atoms. - * `IODINE` excludes mutation steps that add iodine atoms. - * `CHLORINE` excludes mutation steps that add chlorine atoms. - """ + """Removed along with generative design; kept so scripts reach the removal error.""" DOUBLE_BONDS = "DOUBLE_BONDS" TRIPLE_BONDS = "TRIPLE_BONDS" @@ -60,87 +44,3 @@ class StructureExclusion(BaseEnumeration): BROMINE = "BROMINE" IODINE = "IODINE" CHLORINE = "CHLORINE" - - -class GenerativeDesignResult(Serializable["GenerativeDesignResult"]): - """A Citrine Generation Design Execution Result. - - This class represents the result of a generative design execution. - """ - - @classmethod - def _pre_build(cls, data: dict) -> dict: - """Run data modification before building.""" - result = data.pop("result") - data.update(result) - return data - - uid = properties.UUID("id") - execution_id = properties.UUID("execution_id") - - seed = properties.String("seed") - """The seed used to generate the molecule.""" - mutated = properties.String("mutated") - """The mutated molecule.""" - fingerprint_similarity = properties.Float("fingerprint_similarity") - """The fingerprint similarity between the seed and the mutated molecule.""" - fingerprint_type = properties.String("fingerprint_type") - """The fingerprint type used to calculate the fingerprint similarity.""" - - def __init__(self): - pass # pragma: no cover - - -class GenerativeDesignInput(Serializable["GenerativeDesignInput"]): - """A Citrine Generative Design Execution Input. - - Parameters - ---------- - seeds: list[str] - The seeds used to generate the molecules. - fingerprint_type: FingerprintType - The fingerprint type used to calculate the fingerprint similarity. - min_fingerprint_similarity: float - The minimum fingerprint similarity between the seed and the mutated molecule. - mutation_per_seed: int - The number of initial mutations that will be attempted per seed. - IMPORTANT, the total number of molecules generated will likely be lower than this value. - Some mutations may be duplicates or may not meet the fingerprint similarity threshold. - structure_exclusions: list[StructureExclusion] - The structure exclusions used to limit molecule mutations. - If None, no structure exclusions will be used. - min_substructure_counts: dict[str, int] - Dictionary for constraining which substructures (represented by SMARTS strings, - not SMILES) must appear in each mutated molecule, along with integer values - representing the minimum number of times each substructure must appear in a - molecule to be considered a valid mutation. - - """ - - seeds = properties.List(properties.String(), "seeds") - fingerprint_type = properties.Enumeration(FingerprintType, "fingerprint_type") - min_fingerprint_similarity = properties.Float("min_fingerprint_similarity") - mutation_per_seed = properties.Integer("mutation_per_seed") - structure_exclusions = properties.List( - properties.Enumeration(StructureExclusion), "structure_exclusions" - ) - min_substructure_counts = properties.Mapping( - properties.String(), properties.Integer(), "min_substructure_counts" - ) - - def __init__( - self, - *, - seeds: list[str], - fingerprint_type: FingerprintType, - min_fingerprint_similarity: float, - mutation_per_seed: int, - structure_exclusions: list[StructureExclusion] | None = None, - min_substructure_counts: dict[str, int] | None = None, - ): - self.seeds: list[str] = seeds - self.fingerprint_type: FingerprintType = fingerprint_type - self.min_fingerprint_similarity: float = min_fingerprint_similarity - self.mutation_per_seed: int = mutation_per_seed - self.structure_exclusions: list[StructureExclusion] = structure_exclusions or [] - self.min_substructure_counts: dict[str, int] = min_substructure_counts or {} diff --git a/src/citrine/jobs/waiting.py b/src/citrine/jobs/waiting.py index efbe60965..68d6afed9 100644 --- a/src/citrine/jobs/waiting.py +++ b/src/citrine/jobs/waiting.py @@ -5,12 +5,8 @@ from citrine._rest.collection import Collection from citrine.informatics.executions import PredictorEvaluation from citrine.informatics.executions.design_execution import DesignExecution -from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution -from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution -ExecutionType = ( - PredictorEvaluation | DesignExecution | GenerativeDesignExecution | SampleDesignSpaceExecution -) +ExecutionType = PredictorEvaluation | DesignExecution class ConditionTimeoutError(RuntimeError): diff --git a/src/citrine/resources/generative_design_execution.py b/src/citrine/resources/generative_design_execution.py deleted file mode 100644 index 07ffd6339..000000000 --- a/src/citrine/resources/generative_design_execution.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Resources that represent both individual and collections of design workflow executions.""" - -from collections.abc import Iterator -from uuid import UUID - -from citrine._rest.collection import Collection -from citrine._session import Session -from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution -from citrine.informatics.generative_design import GenerativeDesignInput -from citrine.resources.response import Response - - -class GenerativeDesignExecutionCollection(Collection["GenerativeDesignExecution"]): - """A collection of GenerativeDesignExecutions.""" - - _path_template = "/projects/{project_id}/generative-design/executions" - _individual_key = None - _collection_key = "response" - _resource = GenerativeDesignExecution - - def __init__(self, project_id: UUID, session: Session): - self.project_id: UUID = project_id - self.session: Session = session - - def build(self, data: dict) -> GenerativeDesignExecution: - """Build an individual GenerativeDesignExecution.""" - execution = GenerativeDesignExecution.build(data) - execution._session = self.session - execution.project_id = self.project_id - return execution - - def trigger( - self, generative_design_execution_input: GenerativeDesignInput - ) -> GenerativeDesignExecution: - """Trigger a Generative Design execution.""" - path = self._get_path() - request_dict = generative_design_execution_input.dump() - data = self.session.post_resource(path, request_dict) - return self.build(data) - - def register(self, model: GenerativeDesignExecution) -> GenerativeDesignExecution: - """Cannot register an execution.""" - raise NotImplementedError("Cannot register a GenerativeDesignExecution.") - - def update(self, model: GenerativeDesignExecution) -> GenerativeDesignExecution: - """Cannot update an execution.""" - raise NotImplementedError("Cannot update a GenerativeDesignExecution.") - - def list(self, *, per_page: int = 10) -> Iterator[GenerativeDesignExecution]: - """ - Paginate over the elements of the collection. - - Leaving page and per_page as default values will yield all elements in the - collection, paginating over all available pages. - - Parameters - --------- - per_page: int, optional - Max number of results to return per page. Default is 100. This parameter - is used when making requests to the backend service. If the page parameter - is specified it limits the maximum number of elements in the response. - - Returns - ------- - Iterator[ResourceType] - Resources in this collection. - - """ - return self._paginator.paginate( - page_fetcher=self._fetch_page, - collection_builder=self._build_collection_elements, - per_page=per_page, - ) - - def delete(self, uid: UUID | str) -> Response: - """Generative Design Executions cannot be deleted or archived.""" - raise NotImplementedError("Generative Design Executions cannot be deleted") diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index 9e2c572db..767645410 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -9,12 +9,13 @@ from citrine._serialization import properties from citrine._session import Session from citrine._utils.functions import format_escaped_url +from citrine.exceptions import FeatureRemovedError +from citrine.informatics.generative_design import GENERATIVE_DESIGN_REMOVED from citrine.resources.branch import BranchCollection from citrine.resources.descriptors import DescriptorMethods from citrine.resources.design_space import DesignSpaceCollection from citrine.resources.design_workflow import DesignWorkflowCollection from citrine.resources.gemtables import GemTableCollection -from citrine.resources.generative_design_execution import GenerativeDesignExecutionCollection from citrine.resources.predictor import PredictorCollection from citrine.resources.predictor_evaluation import PredictorEvaluationCollection from citrine.resources.project_member import ProjectMember @@ -127,9 +128,9 @@ def design_workflows(self) -> DesignWorkflowCollection: return DesignWorkflowCollection(project_id=self.uid, session=self.session) @property - def generative_design_executions(self) -> GenerativeDesignExecutionCollection: - """Return a collection representing all visible generative design executions.""" - return GenerativeDesignExecutionCollection(project_id=self.uid, session=self.session) + def generative_design_executions(self): + """Removed. Accessing it raises :class:`~citrine.exceptions.FeatureRemovedError`.""" + raise FeatureRemovedError(GENERATIVE_DESIGN_REMOVED) @property def tables(self) -> GemTableCollection: diff --git a/src/citrine/resources/sample_design_space_execution.py b/src/citrine/resources/sample_design_space_execution.py deleted file mode 100644 index f5bfd241f..000000000 --- a/src/citrine/resources/sample_design_space_execution.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Resources that represent both individual and collections of sample design space executions.""" - -from collections.abc import Iterator -from uuid import UUID - -from citrine._rest.collection import Collection -from citrine._session import Session -from citrine.informatics.design_spaces.sample_design_space import SampleDesignSpaceInput -from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution -from citrine.resources.response import Response - - -class SampleDesignSpaceExecutionCollection(Collection["SampleDesignSpaceExecution"]): - """A collection of SampleDesignSpaceExecutions.""" - - _api_version = "v3" - _path_template = "/projects/{project_id}/design-spaces/{design_space_id}/sample" - _individual_key = None - _collection_key = "response" - _resource = SampleDesignSpaceExecution - - def __init__(self, project_id: UUID, design_space_id: UUID, session: Session): - self.project_id: UUID = project_id - self.design_space_id: UUID = design_space_id - self.session: Session = session - - def build(self, data: dict) -> SampleDesignSpaceExecution: - """Build an individual SampleDesignSpaceExecution.""" - execution = SampleDesignSpaceExecution.build(data) - execution._session = self.session - execution.project_id = self.project_id - execution.design_space_id = self.design_space_id - return execution - - def trigger( - self, sample_design_space_input: SampleDesignSpaceInput - ) -> SampleDesignSpaceExecution: - """Trigger a sample design space execution.""" - path = self._get_path() - request_dict = sample_design_space_input.dump() - data = self.session.post_resource(path, request_dict, version=self._api_version) - return self.build(data) - - def register(self, model: SampleDesignSpaceExecution) -> SampleDesignSpaceExecution: - """Cannot register an execution.""" - raise NotImplementedError("Cannot register a SampleDesignSpaceExecution.") - - def update(self, model: SampleDesignSpaceExecution) -> SampleDesignSpaceExecution: - """Cannot update an execution.""" - raise NotImplementedError("Cannot update a SampleDesignSpaceExecution.") - - def list(self, *, per_page: int = 10) -> Iterator[SampleDesignSpaceExecution]: - """ - Paginate over the elements of the collection. - - Leaving page and per_page as default values will yield all elements in the - collection, paginating over all available pages. - - Parameters - --------- - per_page: int, optional - Max number of results to return per page. Default is 100. This parameter - is used when making requests to the backend service. If the page parameter - is specified it limits the maximum number of elements in the response. - - Returns - ------- - Iterator[ResourceType] - Resources in this collection. - - """ - return self._paginator.paginate( - page_fetcher=self._fetch_page, - collection_builder=self._build_collection_elements, - per_page=per_page, - ) - - def delete(self, uid: UUID | str) -> Response: - """Sample Design Space Executions cannot be deleted or archived.""" - raise NotImplementedError("Sample Design Space Executions cannot be deleted") diff --git a/tests/conftest.py b/tests/conftest.py index 77add5893..81331eb0c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -628,22 +628,6 @@ def example_holdout_result_dict(example_holdout_evaluator_dict, example_rmse_met } -@pytest.fixture -def sample_design_space_execution_dict(generic_entity): - ret = generic_entity.copy() - ret.update( - { - "design_space_id": str(uuid.uuid4()), - "status": { - "major": ret.get("status"), - "minor": ret.get("status_description"), - "detail": ret.get("status_detail"), - }, - } - ) - return ret - - @pytest.fixture() def example_design_material(): return { @@ -721,20 +705,6 @@ def example_candidates(example_design_material): } -@pytest.fixture() -def example_sample_design_space_response(example_hierarchical_design_material): - return { - "per_page": 4, - "response": [ - { - "id": str(uuid.uuid4()), - "execution_id": str(uuid.uuid4()), - "material": example_hierarchical_design_material, - } - ], - } - - @pytest.fixture def generic_entity(): user = str(uuid.uuid4()) @@ -769,29 +739,3 @@ def design_execution_dict(generic_entity): } ) return ret - - -@pytest.fixture -def generative_design_execution_dict(generic_entity): - ret = generic_entity.copy() - return ret - - -@pytest.fixture -def example_generation_results(): - return { - "page": 1, - "per_page": 4, - "response": [ - { - "id": str(uuid.uuid4()), - "execution_id": str(uuid.uuid4()), - "result": { - "seed": "CCCCO", - "mutated": "CCCN", - "fingerprint_similarity": 0.41, - "fingerprint_type": "ECFP4", - }, - } - ], - } diff --git a/tests/resources/test_generative_design_execution.py b/tests/resources/test_generative_design_execution.py deleted file mode 100644 index fb321652a..000000000 --- a/tests/resources/test_generative_design_execution.py +++ /dev/null @@ -1,132 +0,0 @@ -import uuid - -import pytest - -from citrine.informatics.executions.generative_design_execution import GenerativeDesignExecution -from citrine.informatics.generative_design import ( - FingerprintType, - GenerativeDesignInput, - StructureExclusion, -) -from citrine.resources.generative_design_execution import GenerativeDesignExecutionCollection -from tests.utils.session import FakeCall, FakeSession - - -@pytest.fixture -def session() -> FakeSession: - return FakeSession() - - -@pytest.fixture -def collection(session) -> GenerativeDesignExecutionCollection: - return GenerativeDesignExecutionCollection(project_id=uuid.uuid4(), session=session) - - -@pytest.fixture -def generative_design_execution( - collection: GenerativeDesignExecutionCollection, generative_design_execution_dict -) -> GenerativeDesignExecution: - return collection.build(generative_design_execution_dict) - - -def test_basic_methods(generative_design_execution, collection): - assert "GenerativeDesignExecution" in str(generative_design_execution) - - with pytest.raises(NotImplementedError): - collection.register(generative_design_execution) - - with pytest.raises(NotImplementedError): - collection.update(generative_design_execution) - - -def test_build_new_execution(collection, generative_design_execution_dict): - execution: GenerativeDesignExecution = collection.build(generative_design_execution_dict) - - assert str(execution.uid) == generative_design_execution_dict["id"] - assert execution.project_id == collection.project_id - assert execution._session == collection.session - assert execution.in_progress() and not execution.succeeded() and not execution.failed() - assert execution.status_detail - - -def test_trigger_execution( - collection: GenerativeDesignExecutionCollection, generative_design_execution_dict, session -): - # Given - session.set_response(generative_design_execution_dict) - design_execution_input = GenerativeDesignInput( - seeds=["CC(O)=O"], - fingerprint_type=FingerprintType.ECFP4, - min_fingerprint_similarity=0.5, - mutation_per_seed=2, - structure_exclusions=[StructureExclusion.BROMINE, StructureExclusion.IODINE], - min_substructure_counts={"C": 1, "O": 1}, - ) - - # When - actual_execution = collection.trigger(design_execution_input) - - # Then - assert str(actual_execution.uid) == generative_design_execution_dict["id"] - expected_path = f"/projects/{collection.project_id}/generative-design/executions" - assert session.last_call == FakeCall( - method="POST", - path=expected_path, - json={ - "seeds": design_execution_input.seeds, - "fingerprint_type": design_execution_input.fingerprint_type.value, - "min_fingerprint_similarity": design_execution_input.min_fingerprint_similarity, - "mutation_per_seed": design_execution_input.mutation_per_seed, - "structure_exclusions": [ - exclusion.value for exclusion in design_execution_input.structure_exclusions - ], - "min_substructure_counts": design_execution_input.min_substructure_counts, - }, - ) - - -def test_generative_design_execution_results( - generative_design_execution: GenerativeDesignExecution, session, example_generation_results -): - # Given - session.set_response(example_generation_results) - - # When - list(generative_design_execution.results(per_page=4)) - - # Then - expected_path = f"/projects/{generative_design_execution.project_id}/generative-design/executions/{generative_design_execution.uid}/results" - assert session.last_call == FakeCall( - method="GET", path=expected_path, params={"per_page": 4, "page": 1} - ) - - -def test_generative_design_execution_result( - generative_design_execution: GenerativeDesignExecution, session, example_generation_results -): - # Given - session.set_response(example_generation_results["response"][0]) - - # When - result_id = example_generation_results["response"][0]["id"] - generative_design_execution.result(result_id=result_id) - - # Then - expected_path = f"/projects/{generative_design_execution.project_id}/generative-design/executions/{generative_design_execution.uid}/results/{result_id}" - assert session.last_call == FakeCall(method="GET", path=expected_path) - - -def test_list(collection: GenerativeDesignExecutionCollection, session): - session.set_response({"page": 1, "per_page": 4, "next": "", "response": []}) - lst = list(collection.list(per_page=4)) - assert len(lst) == 0 - - expected_path = f"/projects/{collection.project_id}/generative-design/executions" - assert session.last_call == FakeCall( - method="GET", path=expected_path, params={"page": 1, "per_page": 4} - ) - - -def test_delete(collection): - with pytest.raises(NotImplementedError): - collection.delete(uuid.uuid4()) diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index 785922a22..4a2b28351 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -142,10 +142,6 @@ def test_design_workflows_get_branch(project): assert None is project.design_workflows.branch_version -def test_generative_design_executions_get_project_id(project): - assert project.uid == project.generative_design_executions.project_id - - def test_branches_get_project_id(project): assert project.uid == project.branches.project_id diff --git a/tests/resources/test_sample_design_space.py b/tests/resources/test_sample_design_space.py deleted file mode 100644 index 131e04d0b..000000000 --- a/tests/resources/test_sample_design_space.py +++ /dev/null @@ -1,133 +0,0 @@ -import uuid - -import pytest - -from citrine.informatics.design_spaces.sample_design_space import SampleDesignSpaceInput -from citrine.informatics.design_spaces.top_level_design_space import TopLevelDesignSpace -from citrine.informatics.executions.sample_design_space_execution import SampleDesignSpaceExecution -from citrine.resources.sample_design_space_execution import SampleDesignSpaceExecutionCollection -from tests.utils.session import FakeCall, FakeSession - - -@pytest.fixture -def session() -> FakeSession: - return FakeSession() - - -@pytest.fixture -def collection(session) -> SampleDesignSpaceExecutionCollection: - ds = TopLevelDesignSpace() - ds._project_id = uuid.uuid4() - ds.uid = uuid.uuid4() - ds._session = session - return ds.sample_design_space_executions - - -@pytest.fixture -def sample_design_space_execution( - collection: SampleDesignSpaceExecutionCollection, sample_design_space_execution_dict -) -> SampleDesignSpaceExecution: - return collection.build(sample_design_space_execution_dict) - - -def test_basic_methods(sample_design_space_execution, collection): - assert "SampleDesignSpaceExecution" in str(sample_design_space_execution) - - with pytest.raises(NotImplementedError): - collection.register(sample_design_space_execution) - - with pytest.raises(NotImplementedError): - collection.update(sample_design_space_execution) - - -def test_build_new_execution(collection, sample_design_space_execution_dict): - execution: SampleDesignSpaceExecution = collection.build(sample_design_space_execution_dict) - - assert str(execution.uid) == sample_design_space_execution_dict["id"] - assert execution.project_id == collection.project_id - assert execution._session == collection.session - assert execution.in_progress() and not execution.succeeded() and not execution.failed() - - -def test_trigger_execution( - collection: SampleDesignSpaceExecutionCollection, sample_design_space_execution_dict, session -): - # Given - session.set_response(sample_design_space_execution_dict) - sample_design_space_execution_input = SampleDesignSpaceInput(n_candidates=10) - - # When - actual_execution = collection.trigger(sample_design_space_execution_input) - - # Then - assert str(actual_execution.uid) == sample_design_space_execution_dict["id"] - expected_path = ( - f"/projects/{collection.project_id}/design-spaces/{collection.design_space_id}/sample" - ) - assert session.last_call == FakeCall( - method="POST", - path=expected_path, - json={"n_candidates": sample_design_space_execution_input.n_candidates}, - ) - - -def test_execution_completes(): - data_success = { - "id": str(uuid.uuid4()), - "status": {"major": "SUCCEEDED", "minor": "COMPLETED", "detail": [], "info": []}, - } - execution_success = SampleDesignSpaceExecution.build(data_success) - assert execution_success.succeeded() - - -def test_sample_design_space_execution_results( - sample_design_space_execution: SampleDesignSpaceExecution, - session, - example_sample_design_space_response, -): - # Given - session.set_response(example_sample_design_space_response) - - # When - list(sample_design_space_execution.results(per_page=4)) - - # Then - expected_path = f"/projects/{sample_design_space_execution.project_id}/design-spaces/{sample_design_space_execution.design_space_id}/sample/{sample_design_space_execution.uid}/results" - assert session.last_call == FakeCall( - method="GET", path=expected_path, params={"page": 1, "per_page": 4} - ) - - -def test_sample_design_space_execution_result( - sample_design_space_execution: SampleDesignSpaceExecution, - session, - example_sample_design_space_response, -): - # Given - session.set_response(example_sample_design_space_response["response"][0]) - - # When - result_id = example_sample_design_space_response["response"][0]["id"] - sample_design_space_execution.result(result_id=result_id) - - # Then - expected_path = f"/projects/{sample_design_space_execution.project_id}/design-spaces/{sample_design_space_execution.design_space_id}/sample/{sample_design_space_execution.uid}/results/{result_id}" - assert session.last_call == FakeCall(method="GET", path=expected_path) - - -def test_list(collection: SampleDesignSpaceExecutionCollection, session): - session.set_response({"page": 1, "per_page": 4, "next": "", "response": []}) - lst = list(collection.list(per_page=4)) - assert len(lst) == 0 - - expected_path = ( - f"/projects/{collection.project_id}/design-spaces/{collection.design_space_id}/sample" - ) - assert session.last_call == FakeCall( - method="GET", path=expected_path, params={"page": 1, "per_page": 4} - ) - - -def test_delete(collection): - with pytest.raises(NotImplementedError): - collection.delete(uuid.uuid4()) diff --git a/tests/test_removed_features.py b/tests/test_removed_features.py new file mode 100644 index 000000000..c8dd586fb --- /dev/null +++ b/tests/test_removed_features.py @@ -0,0 +1,85 @@ +"""Removed platform features must fail locally with the platform's own message.""" + +import re +import uuid + +import pytest + +from citrine.exceptions import CitrineException, FeatureRemovedError +from citrine.informatics.design_spaces import ProductDesignSpace +from citrine.informatics.design_spaces.sample_design_space import SampleDesignSpaceInput +from citrine.informatics.generative_design import ( + FingerprintType, + GenerativeDesignInput, + StructureExclusion, +) +from citrine.resources.project import Project +from tests.utils.session import FakeSession + +GENERATIVE_DESIGN_REMOVED = "Generative design has been removed from the Citrine Platform." +SAMPLING_REMOVED = "Design space sampling has been removed from the Citrine Platform." + + +def raises_removed(message: str): + return pytest.raises(FeatureRemovedError, match=f"^{re.escape(message)}$") + + +@pytest.fixture +def session() -> FakeSession: + return FakeSession() + + +@pytest.fixture +def project(session) -> Project: + project = Project(name="Removed features", session=session, team_id=uuid.uuid4()) + project.uid = uuid.uuid4() + return project + + +@pytest.fixture +def design_space(session, project) -> ProductDesignSpace: + design_space = ProductDesignSpace(name="Removed features", description="") + design_space.uid = uuid.uuid4() + design_space._project_id = project.uid + design_space._session = session + return design_space + + +def test_removal_error_is_a_citrine_exception(): + assert issubclass(FeatureRemovedError, CitrineException) + + +def test_generative_design_script_fails_at_the_input(session): + with raises_removed(GENERATIVE_DESIGN_REMOVED): + GenerativeDesignInput( + seeds=["C"], + fingerprint_type=FingerprintType.ECFP4, + min_fingerprint_similarity=0.1, + mutation_per_seed=25, + structure_exclusions=[StructureExclusion.IONS], + ) + assert session.num_calls == 0 + + +def test_generative_design_executions_raise(project, session): + with raises_removed(GENERATIVE_DESIGN_REMOVED): + project.generative_design_executions.list() + assert session.num_calls == 0 + + +def test_sampling_script_fails_at_the_input(session): + with raises_removed(SAMPLING_REMOVED): + SampleDesignSpaceInput(n_candidates=50) + assert session.num_calls == 0 + + +def test_sample_design_space_executions_raise(design_space, session): + with raises_removed(SAMPLING_REMOVED): + design_space.sample_design_space_executions.trigger(None) + assert session.num_calls == 0 + + +def test_removed_accessors_are_absent_to_introspection(project, design_space, session): + assert not hasattr(project, "generative_design_executions") + assert not hasattr(design_space, "sample_design_space_executions") + assert session.num_calls == 0