From 96f9cc82a1eb1a68b0ef4a3c948d2a14c92d255a Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Tue, 26 May 2026 15:42:29 -0400 Subject: [PATCH 1/2] Deprecate experimental result data sources. This feature will not be directly replaced in the SDK. Users can either utilize the create material from candidate workflow in the UI, or they can manually create GEMD objects from it in the SDK. --- src/citrine/__version__.py | 2 +- src/citrine/informatics/data_sources.py | 8 ++++- src/citrine/informatics/experiment_values.py | 35 +++++++++++++++---- .../informatics/predictors/graph_predictor.py | 2 -- .../resources/experiment_datasource.py | 10 ++++++ src/citrine/resources/predictor.py | 17 +++++++-- tests/informatics/test_data_source.py | 22 +++++++++++- tests/informatics/test_experiment_values.py | 19 +++++----- tests/resources/test_branch.py | 8 +++-- tests/resources/test_experiment_datasource.py | 3 +- tests/resources/test_predictor.py | 19 +++++++++- 11 files changed, 119 insertions(+), 26 deletions(-) diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index c7a18d13e..703970876 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "4.0.3" +__version__ = "4.1.0" diff --git a/src/citrine/informatics/data_sources.py b/src/citrine/informatics/data_sources.py index c9f957cbf..9d26ffe75 100644 --- a/src/citrine/informatics/data_sources.py +++ b/src/citrine/informatics/data_sources.py @@ -2,6 +2,8 @@ from abc import abstractmethod from uuid import UUID +from deprecation import deprecated + from citrine._serialization import properties from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization.serializable import Serializable @@ -115,7 +117,7 @@ def from_gemtable(cls, table: GemTable) -> "GemTableDataSource": class ExperimentDataSourceRef(Serializable['ExperimentDataSourceRef'], DataSource): - """A reference to a data source based on an experiment result hosted on the data platform. + """[DEPRECATED] A reference to a data source based on an experiment result on the platform. Parameters ---------- @@ -129,6 +131,10 @@ class ExperimentDataSourceRef(Serializable['ExperimentDataSourceRef'], DataSourc _data_source_type = "experiments" + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates on the platform. " + "Alternatively, you may convert the candidate into a collection of GEMD " + "objects manually.") def __init__(self, *, datasource_id: UUID): self.datasource_id: UUID = datasource_id diff --git a/src/citrine/informatics/experiment_values.py b/src/citrine/informatics/experiment_values.py index 3a0f90bff..e78f7b8b7 100644 --- a/src/citrine/informatics/experiment_values.py +++ b/src/citrine/informatics/experiment_values.py @@ -1,3 +1,5 @@ +from deprecation import deprecated + from citrine._serialization.serializable import Serializable from citrine._serialization.polymorphic_serializable import PolymorphicSerializable from citrine._serialization import properties @@ -14,11 +16,18 @@ class ExperimentValue(PolymorphicSerializable['ExperimentValue']): - """An container for experiment values. + """[DEPRECATED] An container for experiment values. Abstract type that returns the proper type given a serialized dict. """ + @classmethod + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") + def build(cls, data: dict) -> 'ExperimentValue': + """Build the underlying type.""" + return super().build(data) + @classmethod def get_type(cls, data) -> type[Serializable]: """Return the subtype.""" @@ -67,62 +76,74 @@ def _equals(self, other, attrs) -> bool: class RealExperimentValue(Serializable['RealExperimentValue'], ExperimentValue): - """A floating point experiment result.""" + """[DEPRECATED] A floating point experiment result.""" value = properties.Float('value') typ = properties.String('type', default='RealValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: float): self.value = value class IntegerExperimentValue(Serializable['IntegerExperimentValue'], ExperimentValue): - """An integer value experiment result.""" + """[DEPRECATED] An integer value experiment result.""" value = properties.Integer('value') typ = properties.String('type', default='IntegerValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: int): self.value = value class CategoricalExperimentValue(Serializable['CategoricalExperimentValue'], ExperimentValue): - """An experiment result with a categorical value.""" + """[DEPRECATED] An experiment result with a categorical value.""" value = properties.String('value') typ = properties.String('type', default='CategoricalValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: str): self.value = value class MixtureExperimentValue(Serializable['MixtureExperimentValue'], ExperimentValue): - """An experiment result mapping ingredients and labels to real values.""" + """[DEPRECATED] An experiment result mapping ingredients and labels to real values.""" value = properties.Mapping(properties.String, properties.Float, 'value') typ = properties.String('type', default='MixtureValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: dict[str, float]): self.value = value class ChemicalFormulaExperimentValue(Serializable['ChemicalFormulaExperimentValue'], ExperimentValue): - """Experiment value for a chemical formula.""" + """[DEPRECATED] Experiment value for a chemical formula.""" value = properties.String('value') typ = properties.String('type', default='InorganicValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: str): self.value = value class MolecularStructureExperimentValue(Serializable['MolecularStructureExperimentValue'], ExperimentValue): - """Experiment value for a molecular structure.""" + """[DEPRECATED] Experiment value for a molecular structure.""" value = properties.String('value') typ = properties.String('type', default='OrganicValue', deserializable=False) + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, value: str): self.value = value diff --git a/src/citrine/informatics/predictors/graph_predictor.py b/src/citrine/informatics/predictors/graph_predictor.py index a636e1b7b..3ebb4c6bf 100644 --- a/src/citrine/informatics/predictors/graph_predictor.py +++ b/src/citrine/informatics/predictors/graph_predictor.py @@ -48,8 +48,6 @@ class GraphPredictor(VersionedEngineResource['GraphPredictor'], AsynchronousObje description = properties.Optional(properties.String(), 'data.description') predictors = properties.List(properties.Object(PredictorNode), 'data.instance.predictors') - # the default seems to be defined in instances, not the class itself - # this is tested in test_graph_default_training_data training_data = properties.List( properties.Object(DataSource), 'data.instance.training_data', default=[] ) diff --git a/src/citrine/resources/experiment_datasource.py b/src/citrine/resources/experiment_datasource.py index 5171c111f..467dcc10d 100644 --- a/src/citrine/resources/experiment_datasource.py +++ b/src/citrine/resources/experiment_datasource.py @@ -5,6 +5,8 @@ from io import StringIO from uuid import UUID +from deprecation import deprecated + from citrine._rest.collection import Collection from citrine._serialization import properties from citrine._serialization.serializable import Serializable @@ -32,6 +34,8 @@ class CandidateExperimentSnapshot(Serializable['CandidateExperimentSnapshot']): 'overrides') """:dict[str, ExperimentValue]: dictionary of candidate material variable overrides""" + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, *args, **kwargs): """Candidate experiment snapshots are not directly instantiated by the user.""" pass # pragma: no cover @@ -58,6 +62,8 @@ class ExperimentDataSource(Serializable['ExperimentDataSource']): create_time = properties.Datetime('metadata.created.time', serializable=False) """:datetime: date and time at which this data source was created""" + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates.") def __init__(self, *args, **kwargs): """Experiment data sources are not directly instantiated by the user.""" pass # pragma: no cover @@ -93,6 +99,10 @@ class ExperimentDataSourceCollection(Collection[ExperimentDataSource]): _resource = ExperimentDataSource _collection_key = 'response' + @deprecated(deprecated_in="4.1.0", removed_in="5.0.0", + details="Replaced by creating materials from candidates on the platform. " + "Alternatively, you may convert the candidate into a collection of GEMD " + "objects manually.") def __init__(self, project_id: UUID, session: Session): self.project_id = project_id self.session: Session = session diff --git a/src/citrine/resources/predictor.py b/src/citrine/resources/predictor.py index 4125f052c..fad6dd839 100644 --- a/src/citrine/resources/predictor.py +++ b/src/citrine/resources/predictor.py @@ -1,4 +1,5 @@ """Resources that represent collections of predictors.""" +import warnings from collections.abc import Iterable from functools import partial from typing import Any @@ -11,7 +12,7 @@ from citrine._rest.paginator import Paginator from citrine._serialization import properties from citrine._session import Session -from citrine.informatics.data_sources import DataSource +from citrine.informatics.data_sources import DataSource, ExperimentDataSourceRef from citrine.informatics.design_candidate import HierarchicalDesignMaterial from citrine.informatics.predictors import GraphPredictor from citrine.resources.status_detail import StatusDetail @@ -107,6 +108,16 @@ def _page_fetcher(self, *, uid: UUID | str, **additional_params): } return partial(self._fetch_page, **fetcher_params) + def _check_data_sources(self, predictor: GraphPredictor): + for data_source in predictor.training_data: + print(data_source) + if isinstance(data_source, ExperimentDataSourceRef): + warnings.warn("This predictor contains an experiment result, which is being " + "replaced by creating materials from candidates on the platform. " + "Alternatively, you may convert the candidate into a collection of " + "GEMD objects manually.", + DeprecationWarning) + def build(self, data: dict) -> GraphPredictor: """Build an individual Predictor.""" predictor: GraphPredictor = GraphPredictor.build(data) @@ -120,7 +131,9 @@ def get(self, version: int | str = MOST_RECENT_VER) -> GraphPredictor: path = self._construct_path(uid, version) entity = self.session.get_resource(path, version=self._api_version) - return self.build(entity) + predictor = self.build(entity) + self._check_data_sources(predictor) + return predictor def get_featurized_training_data( self, diff --git a/tests/informatics/test_data_source.py b/tests/informatics/test_data_source.py index 7ca003371..cd0914f1a 100644 --- a/tests/informatics/test_data_source.py +++ b/tests/informatics/test_data_source.py @@ -15,12 +15,16 @@ @pytest.fixture(params=[ GemTableDataSource(table_id=uuid.uuid4(), table_version=1), GemTableDataSource(table_id=uuid.uuid4(), table_version="2"), - ExperimentDataSourceRef(datasource_id=uuid.uuid4()), SnapshotDataSource(snapshot_id=uuid.uuid4()) ]) def data_source(request): return request.param +@pytest.fixture +def deprecated_data_source(): + with pytest.deprecated_call(): + return ExperimentDataSourceRef(datasource_id=uuid.uuid4()) + def test_deser_from_parent(data_source): # Serialize and deserialize the descriptors, making sure they are round-trip serializable @@ -29,11 +33,23 @@ def test_deser_from_parent(data_source): assert data_source == data_source_deserialized +def test_deser_from_parent_deprecated(deprecated_data_source): + # Serialize and deserialize the descriptors, making sure they are round-trip serializable + data = deprecated_data_source.dump() + data_source_deserialized = DataSource.build(data) + assert deprecated_data_source == data_source_deserialized + + def test_invalid_eq(data_source): other = None assert not data_source == other +def test_invalid_eq(deprecated_data_source): + other = None + assert not deprecated_data_source == other + + def test_invalid_deser(): with pytest.raises(ValueError): DataSource.build({}) @@ -42,6 +58,10 @@ def test_invalid_deser(): DataSource.build({"type": "foo"}) +def test_deprecated_data_source_id(deprecated_data_source): + with pytest.deprecated_call(): + assert deprecated_data_source == DataSource.from_data_source_id(deprecated_data_source.to_data_source_id()) + def test_data_source_id(data_source): assert data_source == DataSource.from_data_source_id(data_source.to_data_source_id()) diff --git a/tests/informatics/test_experiment_values.py b/tests/informatics/test_experiment_values.py index fc2887a8e..0c6427803 100644 --- a/tests/informatics/test_experiment_values.py +++ b/tests/informatics/test_experiment_values.py @@ -12,21 +12,24 @@ @pytest.fixture(params=[ - CategoricalExperimentValue("categorical"), - ChemicalFormulaExperimentValue("(Ca)1(O)3(Si)1"), - IntegerExperimentValue(7), - MixtureExperimentValue({"ingredient1": 0.3, "ingredient2": 0.7}), - MolecularStructureExperimentValue("CC1(CC(CC(N1)(C)C)NCCCCCCNC2CC(NC(C2)(C)C)(C)C)C.C1COCCN1C2=NC(=NC(=N2)Cl)Cl"), - RealExperimentValue(3.5) + (CategoricalExperimentValue, ("categorical", )), + (ChemicalFormulaExperimentValue, ("(Ca)1(O)3(Si)1",)), + (IntegerExperimentValue, (7,)), + (MixtureExperimentValue, ({"ingredient1": 0.3, "ingredient2": 0.7},)), + (MolecularStructureExperimentValue, ("CC1(CC(CC(N1)(C)C)NCCCCCCNC2CC(NC(C2)(C)C)(C)C)C.C1COCCN1C2=NC(=NC(=N2)Cl)Cl",)), + (RealExperimentValue, (3.5,)) ]) def experiment_value(request): - return request.param + cls, args = request.param + with pytest.deprecated_call(): + return cls(*args) def test_deser_from_parent(experiment_value): # Serialize and deserialize the experiment values, making sure they are round-trip serializable data = experiment_value.dump() - experiment_value_deserialized = ExperimentValue.build(data) + with pytest.deprecated_call(): + experiment_value_deserialized = ExperimentValue.build(data) assert experiment_value == experiment_value_deserialized diff --git a/tests/resources/test_branch.py b/tests/resources/test_branch.py index c0feea86b..bd1581861 100644 --- a/tests/resources/test_branch.py +++ b/tests/resources/test_branch.py @@ -537,7 +537,9 @@ def test_experiment_datasource(session, collection): session.set_response({'response': [erds]}) # When / Then - assert branch.experiment_datasource is not None + with pytest.deprecated_call(): + assert branch.experiment_datasource is not None + assert session.calls == [ FakeCall(method='GET', path=erds_path, params={'branch': str(branch.uid), 'version': LATEST_VER, 'per_page': 100, 'page': 1}) ] @@ -550,7 +552,9 @@ def test_no_experiment_datasource(session, collection): session.set_response({'response': []}) # When / Then - assert branch.experiment_datasource is None + with pytest.deprecated_call(): + assert branch.experiment_datasource is None + assert session.calls == [ FakeCall(method='GET', path=erds_path, params={'branch': str(branch.uid), 'version': LATEST_VER, 'per_page': 100, 'page': 1}) ] diff --git a/tests/resources/test_experiment_datasource.py b/tests/resources/test_experiment_datasource.py index b7db06011..4e4e1bf4d 100644 --- a/tests/resources/test_experiment_datasource.py +++ b/tests/resources/test_experiment_datasource.py @@ -23,7 +23,8 @@ def session(): @pytest.fixture def collection(session) -> ExperimentDataSourceCollection: - return ExperimentDataSourceCollection(uuid.uuid4(), session) + with pytest.deprecated_call(): + return ExperimentDataSourceCollection(uuid.uuid4(), session) @pytest.fixture diff --git a/tests/resources/test_predictor.py b/tests/resources/test_predictor.py index efce269ec..9eaf206ec 100644 --- a/tests/resources/test_predictor.py +++ b/tests/resources/test_predictor.py @@ -5,7 +5,7 @@ from copy import deepcopy from citrine.exceptions import BadRequest, Conflict, ModuleRegistrationFailedException, NotFound -from citrine.informatics.data_sources import GemTableDataSource +from citrine.informatics.data_sources import ExperimentDataSourceRef, GemTableDataSource from citrine.informatics.descriptors import RealDescriptor from citrine.informatics.predictors import ( AutoMLPredictor, @@ -754,3 +754,20 @@ def test_rename_description_only(valid_graph_predictor_data): versions_path = _PredictorVersionCollection._path_template.format(project_id=pc.project_id, uid=pred_id) expected_payload = {"name": None, "description": new_description} assert session.calls == [FakeCall(method="PUT", path=f"{versions_path}/{pred_version}/rename", json=expected_payload)] + + +def test_get_predictor_with_experiment_data_source_deprecated(valid_graph_predictor_data): + # Given + session = FakeSession() + pc = PredictorCollection(uuid.uuid4(), session) + + with pytest.deprecated_call(): + erds = ExperimentDataSourceRef(datasource_id=uuid.uuid4()) + entity = deepcopy(valid_graph_predictor_data) + entity["data"]["instance"]["training_data"] = [erds.dump()] + + session.set_responses(entity) + + # When + with pytest.deprecated_call(): + pc.get(uuid.uuid4()) From 09d78b8a4eed4c706451e5aa141a61e7947fcb20 Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 28 May 2026 09:59:46 -0400 Subject: [PATCH 2/2] PR tweaks. --- src/citrine/resources/experiment_datasource.py | 2 +- tests/informatics/test_data_source.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/citrine/resources/experiment_datasource.py b/src/citrine/resources/experiment_datasource.py index 467dcc10d..e78ab0fbd 100644 --- a/src/citrine/resources/experiment_datasource.py +++ b/src/citrine/resources/experiment_datasource.py @@ -92,7 +92,7 @@ def read(self) -> str: class ExperimentDataSourceCollection(Collection[ExperimentDataSource]): - """Represents the collection of all experiment data sources associated with a project.""" + """[DEPRECATED] The collection of all experiment data sources associated with a project.""" _path_template = 'projects/{project_id}/candidate-experiment-datasources' _individual_key = None diff --git a/tests/informatics/test_data_source.py b/tests/informatics/test_data_source.py index cd0914f1a..9bc8f04a2 100644 --- a/tests/informatics/test_data_source.py +++ b/tests/informatics/test_data_source.py @@ -45,11 +45,6 @@ def test_invalid_eq(data_source): assert not data_source == other -def test_invalid_eq(deprecated_data_source): - other = None - assert not deprecated_data_source == other - - def test_invalid_deser(): with pytest.raises(ValueError): DataSource.build({})