diff --git a/modelforge-curate/modelforge/curate/examples/basic_usage.ipynb b/modelforge-curate/modelforge/curate/examples/basic_usage.ipynb index da23e5e6..c9b67252 100644 --- a/modelforge-curate/modelforge/curate/examples/basic_usage.ipynb +++ b/modelforge-curate/modelforge/curate/examples/basic_usage.ipynb @@ -144,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "a5bcd450-3d52-4a45-ab1a-f107e11df202", "metadata": {}, "outputs": [], @@ -155,6 +155,28 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": 9, + "id": "711bec10-63ec-44a8-a368-f38bd3bb3577", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Positions(name='positions', value=array([[[1., 1., 1.],\n", + " [2., 2., 2.]]]), units=, classification='per_atom', property_type='length', n_configs=1, n_atoms=2)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "positions" + ] + }, { "cell_type": "markdown", "id": "144b4f9e-62b8-42b5-8e79-429e133574b4", @@ -168,7 +190,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "id": "e03c8a4f-a49f-444f-8b3c-252d7373e77a", "metadata": {}, "outputs": [], @@ -210,7 +232,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 13, "id": "72291451-a62f-4780-b981-8fcd4127a9f1", "metadata": {}, "outputs": [], @@ -232,7 +254,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 14, "id": "caf4f58d-502a-48ee-baec-e1fb99e094b0", "metadata": {}, "outputs": [ @@ -265,7 +287,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 15, "id": "56329f06-7440-44e9-82aa-4a1a38e1b874", "metadata": {}, "outputs": [], @@ -284,7 +306,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 16, "id": "cf96ffd4-de45-427c-9937-19a3ab60bf41", "metadata": {}, "outputs": [ @@ -322,7 +344,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 17, "id": "38a2bed3-e521-49ae-a2ef-2ecd58d15b84", "metadata": {}, "outputs": [ @@ -340,7 +362,7 @@ " 'meta_data': {}}" ] }, - "execution_count": 12, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -359,7 +381,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 18, "id": "1020acc8-7b71-4ff9-819f-9964713c6314", "metadata": {}, "outputs": [ @@ -398,7 +420,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 19, "id": "1c990c88-4230-48b0-b622-a97030a3ea4a", "metadata": {}, "outputs": [ @@ -591,7 +613,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "bcb5c1dd-49dc-448a-a861-9a39c9b28c5d", "metadata": {}, "outputs": [ @@ -601,7 +623,7 @@ "True" ] }, - "execution_count": 19, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -963,7 +985,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 30, "id": "552fc18d-288d-40fd-8a7e-d17832989478", "metadata": {}, "outputs": [ @@ -996,7 +1018,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 31, "id": "f08f8aa5-f3ba-4cb9-91e9-e493513167f3", "metadata": {}, "outputs": [], @@ -1016,7 +1038,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 32, "id": "82df5525-ce64-41d3-9d71-93b8fdfeb3a4", "metadata": {}, "outputs": [ @@ -1045,7 +1067,7 @@ "* meta_data: ([])" ] }, - "execution_count": 33, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -1064,7 +1086,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 33, "id": "7a8ce2cd-a380-43ea-9605-de6d0efe5b1f", "metadata": {}, "outputs": [ @@ -1131,14 +1153,14 @@ "outputs": [ { "ename": "ValidationError", - "evalue": "1 validation error for Positions\n Value error, Unit angstrom ** 2 of positions are not compatible with the property type length.\n [type=value_error, input_value={'value': [[[1.0, 1.0, 1....}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.8/v/value_error", + "evalue": "1 validation error for Positions\n Value error, Unit angstrom ** 2 of positions are not compatible with the property type length.\n [type=value_error, input_value={'value': [[[1.0, 1.0, 1....}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/value_error", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValidationError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[35], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m pos \u001b[38;5;241m=\u001b[39m \u001b[43mPositions\u001b[49m\u001b[43m(\u001b[49m\u001b[43mvalue\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[43m[\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m1.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1.0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m2.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2.0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m3.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3.0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43munits\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43munit\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mangstrom\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43munit\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mangstrom\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/anaconda3/envs/modelforge311/lib/python3.11/site-packages/pydantic/main.py:193\u001b[0m, in \u001b[0;36mBaseModel.__init__\u001b[0;34m(self, **data)\u001b[0m\n\u001b[1;32m 191\u001b[0m \u001b[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001b[39;00m\n\u001b[1;32m 192\u001b[0m __tracebackhide__ \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m--> 193\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__pydantic_validator__\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate_python\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mself_instance\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n", - "\u001b[0;31mValidationError\u001b[0m: 1 validation error for Positions\n Value error, Unit angstrom ** 2 of positions are not compatible with the property type length.\n [type=value_error, input_value={'value': [[[1.0, 1.0, 1....}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.8/v/value_error" + "File \u001b[0;32m/opt/anaconda3/envs/modelforge311/lib/python3.12/site-packages/pydantic/main.py:214\u001b[0m, in \u001b[0;36mBaseModel.__init__\u001b[0;34m(self, **data)\u001b[0m\n\u001b[1;32m 212\u001b[0m \u001b[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001b[39;00m\n\u001b[1;32m 213\u001b[0m __tracebackhide__ \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[0;32m--> 214\u001b[0m validated_self \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__pydantic_validator__\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate_python\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mself_instance\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 215\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m validated_self:\n\u001b[1;32m 216\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 217\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mA custom validator is returning a value other than `self`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m 218\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mReturning anything other than `self` from a top level model validator isn\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt supported when validating via `__init__`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 219\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mSee the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 220\u001b[0m stacklevel\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m2\u001b[39m,\n\u001b[1;32m 221\u001b[0m )\n", + "\u001b[0;31mValidationError\u001b[0m: 1 validation error for Positions\n Value error, Unit angstrom ** 2 of positions are not compatible with the property type length.\n [type=value_error, input_value={'value': [[[1.0, 1.0, 1....}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.10/v/value_error" ] } ], @@ -1435,7 +1457,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.12.8" } }, "nbformat": 4, diff --git a/modelforge-curate/modelforge/curate/properties.py b/modelforge-curate/modelforge/curate/properties.py index e78b35ac..d86f5c04 100644 --- a/modelforge-curate/modelforge/curate/properties.py +++ b/modelforge-curate/modelforge/curate/properties.py @@ -378,7 +378,11 @@ class TotalCharge(PropertyBaseModel): def _check_charge_shape(self) -> Self: if self.value.shape[1] != 1: raise ValueError( - f"Shape of charge should be [n_configs, 1], found {len(self.value.shape)}" + f"Shape of charge should be [n_configs, 1], found {self.value.shape}" + ) + if len(self.value.shape) != 2: + raise ValueError( + f"Shape of charge should be 2d, found {len(self.value.shape)}" ) return self @@ -688,7 +692,11 @@ class Polarizability(PropertyBaseModel): def _check_polarizability_shape(self) -> Self: if self.value.shape[1] != 1: raise ValueError( - f"Shape of polarizability should be [n_configs, 1], found {len(self.value.shape)}" + f"Shape of polarizability should be [n_configs, 1], found {self.value.shape}" + ) + if len(self.value.shape) != 2: + raise ValueError( + f"Shape of polarizability should be 2d, found {len(self.value.shape)}" ) return self diff --git a/modelforge-curate/modelforge/curate/tests/test_curate.py b/modelforge-curate/modelforge/curate/tests/test_curate.py index 8eb6b85a..89ffcae8 100644 --- a/modelforge-curate/modelforge/curate/tests/test_curate.py +++ b/modelforge-curate/modelforge/curate/tests/test_curate.py @@ -57,214 +57,6 @@ def test_dataset_create_record(): assert "mol4" in new_dataset.records -def test_initialize_properties(): - positions = Positions(value=[[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]], units="nanometer") - - assert type(positions.value) == np.ndarray - assert positions.units == unit.nanometer - assert positions.value.shape == (1, 2, 3) - assert positions.classification == "per_atom" - assert positions.name == "positions" - assert positions.property_type == "length" - - assert positions.n_atoms == 2 - assert positions.n_configs == 1 - - energies = Energies(value=np.array([[0.1], [0.3]]), units=unit.hartree) - - assert type(energies.value) == np.ndarray - assert energies.units == unit.hartree - assert energies.value.shape == (2, 1) - assert energies.classification == "per_system" - assert energies.name == "energies" - assert energies.property_type == "energy" - assert energies.n_configs == 2 - - atomic_numbers = AtomicNumbers(value=np.array([[1], [6]])) - assert type(atomic_numbers.value) == np.ndarray - assert atomic_numbers.value.shape == (2, 1) - assert atomic_numbers.classification == "atomic_numbers" - assert atomic_numbers.name == "atomic_numbers" - assert atomic_numbers.property_type == "atomic_numbers" - assert atomic_numbers.n_atoms == 2 - - meta_data = MetaData(name="smiles", value="[CH]") - assert meta_data.name == "smiles" - assert meta_data.value == "[CH]" - assert meta_data.classification == "meta_data" - assert meta_data.property_type == "meta_data" - - partial_charges = PartialCharges( - value=np.array([[[0.1], [-0.1]]]), units=unit.elementary_charge - ) - assert partial_charges.value.shape == (1, 2, 1) - assert partial_charges.units == unit.elementary_charge - assert partial_charges.classification == "per_atom" - assert partial_charges.name == "partial_charges" - assert partial_charges.property_type == "charge" - - forces = Forces( - value=np.array([[[0.1, 0.1, 0.1], [-0.1, -0.1, -0.1]]]), - units=unit.kilojoule_per_mole / unit.nanometer, - ) - assert forces.value.shape == (1, 2, 3) - assert forces.units == unit.kilojoule_per_mole / unit.nanometer - assert forces.classification == "per_atom" - assert forces.name == "forces" - assert forces.property_type == "force" - - total_charge = TotalCharge(value=np.array([[[0.1]]]), units=unit.elementary_charge) - assert total_charge.value.shape == (1, 1, 1) - assert total_charge.units == unit.elementary_charge - assert total_charge.classification == "per_system" - assert total_charge.name == "total_charge" - assert total_charge.property_type == "charge" - - dipole_moment = DipoleMomentPerSystem( - value=np.array([[0.1, 0.1, 0.1]]), units=unit.debye - ) - assert dipole_moment.value.shape == (1, 3) - assert dipole_moment.units == unit.debye - assert dipole_moment.classification == "per_system" - assert dipole_moment.name == "dipole_moment_per_system" - assert dipole_moment.property_type == "dipole_moment" - - quadrupole_moment = QuadrupoleMomentPerSystem( - value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), - units=unit.debye * unit.nanometer, - ) - assert quadrupole_moment.value.shape == (1, 3, 3) - assert quadrupole_moment.units == unit.debye * unit.nanometer - assert quadrupole_moment.classification == "per_system" - assert quadrupole_moment.name == "quadrupole_moment_per_system" - assert quadrupole_moment.property_type == "quadrupole_moment" - - dipole_moment_scalar = DipoleMomentScalarPerSystem( - value=np.array([[0.1]]), units=unit.debye - ) - assert dipole_moment_scalar.value.shape == (1, 1) - assert dipole_moment_scalar.units == unit.debye - assert dipole_moment_scalar.classification == "per_system" - assert dipole_moment_scalar.name == "dipole_moment_scalar_per_system" - assert dipole_moment_scalar.property_type == "dipole_moment" - - dipole_moment_per_atom = DipoleMomentPerAtom( - value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), units=unit.debye - ) - assert dipole_moment_per_atom.value.shape == (1, 2, 3) - assert dipole_moment_per_atom.units == unit.debye - assert dipole_moment_per_atom.classification == "per_atom" - assert dipole_moment_per_atom.name == "dipole_moment_per_atom" - assert dipole_moment_per_atom.property_type == "dipole_moment" - assert dipole_moment_per_atom.n_atoms == 2 - assert dipole_moment_per_atom.n_configs == 1 - - quadrupole_moment_per_atom = QuadrupoleMomentPerAtom( - value=np.array( - [ - [ - [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], - [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], - ] - ] - ), - units=unit.debye * unit.nanometer, - ) - assert quadrupole_moment_per_atom.value.shape == (1, 2, 3, 3) - assert quadrupole_moment_per_atom.units == unit.debye * unit.nanometer - assert quadrupole_moment_per_atom.classification == "per_atom" - assert quadrupole_moment_per_atom.name == "quadrupole_moment_per_atom" - assert quadrupole_moment_per_atom.property_type == "quadrupole_moment" - assert quadrupole_moment_per_atom.n_atoms == 2 - assert quadrupole_moment_per_atom.n_configs == 1 - - # octupole moment has value of shape (M, N, 3,3,3) - octupole_moment_per_atom = OctupoleMomentPerAtom( - value=np.array( - [ - [ - [ - [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], - [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], - [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], - ] - ] - ] - ), - units=unit.debye * unit.nanometer**2, - ) - - assert octupole_moment_per_atom.value.shape == (1, 1, 3, 3, 3) - assert octupole_moment_per_atom.units == unit.debye * unit.nanometer**2 - assert octupole_moment_per_atom.classification == "per_atom" - assert octupole_moment_per_atom.name == "octupole_moment_per_atom" - assert octupole_moment_per_atom.property_type == "octupole_moment" - assert octupole_moment_per_atom.n_atoms == 1 - assert octupole_moment_per_atom.n_configs == 1 - - polarizability = Polarizability( - value=np.array([[0.1]]), - units=unit.bohr**3, - ) - assert polarizability.value.shape == (1, 1) - assert polarizability.units == unit.bohr**3 - assert polarizability.classification == "per_system" - assert polarizability.name == "polarizability" - assert polarizability.property_type == "polarizability" - - base_prop = PropertyBaseModel( - name="test_prop", - value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), - units=unit.nanometer, - property_type="length", - classification="per_atom", - ) - assert base_prop.value.shape == (1, 3, 3) - assert base_prop.units == unit.nanometer - assert base_prop.classification == "per_atom" - assert base_prop.name == "test_prop" - assert base_prop.property_type == "length" - - # various tests that should fail based on wrong dimensions or units - with pytest.raises(ValueError): - positions = Positions( - value=[[[1.0, 1.0, 1.0, 2.0], [2.0, 2.0, 2.0, 3.0]]], units="nanometer" - ) - with pytest.raises(ValueError): - positions = Positions(value=[1.0, 1.0, 1.0, 2.0, 2.0, 2.0], units="meter") - # not units! we don't assume, must specify - with pytest.raises(ValueError): - positions = Positions(value=np.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]])) - - # wrong shape - with pytest.raises(ValueError): - energies = Energies(value=np.array([0.1]), units=unit.hartree) - - # wrong shape - with pytest.raises(ValueError): - energies = Energies(value=np.array([[0.1, 0.3]]), units=unit.hartree) - with pytest.raises(ValueError): - atomic_numbers = AtomicNumbers(value=np.array([1, 6])) - # wrong shape - with pytest.raises(ValueError): - atomic_numbers = AtomicNumbers(value=np.array([[1, 6]])) - - # wrong shape - with pytest.raises(ValueError): - atomic_numbers = AtomicNumbers(value=np.array([[1, 6], [1, 6]])) - - # incompatible units - with pytest.raises(ValueError): - energies = Energies( - value=np.array([[0.1], [0.3]]), units=unit.kilojoule_per_mole**2 - ) - # incompatible units - with pytest.raises(ValueError): - positions = Positions( - value=[[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]], units=unit.hartree - ) - - def test_add_properties_to_records_directly(): record = Record(name="mol1") @@ -751,49 +543,6 @@ def test_write_hdf5(prep_temp_dir): assert data["filename"] == "test_dataset.hdf5" -def test_unit_system(): - - assert GlobalUnitSystem.name == "default" - assert GlobalUnitSystem.length == unit.nanometer - assert GlobalUnitSystem.force == unit.kilojoule_per_mole / unit.nanometer - assert GlobalUnitSystem.energy == unit.kilojoule_per_mole - assert GlobalUnitSystem.charge == unit.elementary_charge - assert GlobalUnitSystem.dipole_moment == unit.elementary_charge * unit.nanometer - assert ( - GlobalUnitSystem.quadrupole_moment == unit.elementary_charge * unit.nanometer**2 - ) - assert GlobalUnitSystem.polarizability == unit.nanometer**3 - assert GlobalUnitSystem.atomic_numbers == unit.dimensionless - assert GlobalUnitSystem.dimensionless == unit.dimensionless - - assert GlobalUnitSystem.get_units("length") == unit.nanometer - assert ( - GlobalUnitSystem.get_units("force") == unit.kilojoule_per_mole / unit.nanometer - ) - assert GlobalUnitSystem.get_units("energy") == unit.kilojoule_per_mole - assert GlobalUnitSystem.get_units("charge") == unit.elementary_charge - assert ( - GlobalUnitSystem.get_units("dipole_moment") - == unit.elementary_charge * unit.nanometer - ) - assert ( - GlobalUnitSystem.get_units("quadrupole_moment") - == unit.elementary_charge * unit.nanometer**2 - ) - assert GlobalUnitSystem.get_units("polarizability") == unit.nanometer**3 - assert GlobalUnitSystem.get_units("atomic_numbers") == unit.dimensionless - assert GlobalUnitSystem.get_units("dimensionless") == unit.dimensionless - - GlobalUnitSystem.length = unit.angstrom - assert GlobalUnitSystem.length == unit.angstrom - - GlobalUnitSystem.set_global_units("pressure", unit.bar) - assert GlobalUnitSystem.get_units("pressure") == unit.bar - - GlobalUnitSystem.set_global_units("length", unit.nanometer) - assert GlobalUnitSystem.length == unit.nanometer - - def test_dataset_validation(): new_dataset = SourceDataset("test_dataset") new_dataset.create_record("mol1") diff --git a/modelforge-curate/modelforge/curate/tests/test_properties.py b/modelforge-curate/modelforge/curate/tests/test_properties.py new file mode 100644 index 00000000..88d9fed9 --- /dev/null +++ b/modelforge-curate/modelforge/curate/tests/test_properties.py @@ -0,0 +1,327 @@ +import pytest +import numpy as np +from openff.units import unit + +from modelforge.curate.properties import * + + +def test_initialize_positions(): + positions = Positions(value=[[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]], units="nanometer") + + assert type(positions.value) == np.ndarray + assert positions.units == unit.nanometer + assert positions.value.shape == (1, 2, 3) + assert positions.classification == "per_atom" + assert positions.name == "positions" + assert positions.property_type == "length" + + assert positions.n_atoms == 2 + assert positions.n_configs == 1 + + # wrong value.shape[2] + with pytest.raises(ValueError): + positions = Positions( + value=[[[1.0, 1.0, 1.0, 2.0], [2.0, 2.0, 2.0, 3.0]]], units="nanometer" + ) + # wrong shape + with pytest.raises(ValueError): + positions = Positions(value=[1.0, 1.0, 1.0, 2.0, 2.0, 2.0], units="meter") + + # no units, we don't assume, must specify + with pytest.raises(ValueError): + positions = Positions(value=np.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]])) + + # incompatible units + with pytest.raises(ValueError): + positions = Positions( + value=[[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]], units=unit.hartree + ) + + +def test_initialize_energies(): + energies = Energies(value=np.array([[0.1], [0.3]]), units=unit.hartree) + + assert type(energies.value) == np.ndarray + assert energies.units == unit.hartree + assert energies.value.shape == (2, 1) + assert energies.classification == "per_system" + assert energies.name == "energies" + assert energies.property_type == "energy" + assert energies.n_configs == 2 + + # wrong shape + with pytest.raises(ValueError): + energies = Energies(value=np.array([0.1]), units=unit.hartree) + + # incompatible units + with pytest.raises(ValueError): + energies = Energies( + value=np.array([[0.1], [0.3]]), units=unit.kilojoule_per_mole**2 + ) + + # wrong shape + with pytest.raises(ValueError): + energies = Energies(value=np.array([[0.1, 0.3]]), units=unit.hartree) + + +def test_initialize_atomic_numbers(): + atomic_numbers = AtomicNumbers(value=np.array([[1], [6]])) + assert type(atomic_numbers.value) == np.ndarray + assert atomic_numbers.value.shape == (2, 1) + assert atomic_numbers.classification == "atomic_numbers" + assert atomic_numbers.name == "atomic_numbers" + assert atomic_numbers.property_type == "atomic_numbers" + assert atomic_numbers.n_atoms == 2 + + with pytest.raises(ValueError): + atomic_numbers = AtomicNumbers(value=np.array([1, 6])) + # wrong shape + with pytest.raises(ValueError): + atomic_numbers = AtomicNumbers(value=np.array([[1, 6]])) + + # wrong shape + with pytest.raises(ValueError): + atomic_numbers = AtomicNumbers(value=np.array([[1, 6], [1, 6]])) + + with pytest.raises(ValueError): + atomic_numbers = AtomicNumbers( + value=np.array([[1.0], [6.0]]), units="nanometers" + ) + + +def test_initialize_meta_data(): + meta_data = MetaData(name="smiles", value="[CH]") + assert meta_data.name == "smiles" + assert meta_data.value == "[CH]" + assert meta_data.classification == "meta_data" + assert meta_data.property_type == "meta_data" + + +def test_initialize_partial_charges(): + partial_charges = PartialCharges( + value=np.array([[[0.1], [-0.1]]]), units=unit.elementary_charge + ) + assert partial_charges.value.shape == (1, 2, 1) + assert partial_charges.units == unit.elementary_charge + assert partial_charges.classification == "per_atom" + assert partial_charges.name == "partial_charges" + assert partial_charges.property_type == "charge" + + # wrong shape + with pytest.raises(ValueError): + partial_charges = PartialCharges( + value=np.array([[[0.1, -0.1]]]), units=unit.elementary_charge + ) + + with pytest.raises(ValueError): + partial_charges = PartialCharges( + value=np.array([0.1]), units=unit.elementary_charge + ) + + +def test_initialize_forces(): + forces = Forces( + value=np.array([[[0.1, 0.1, 0.1], [-0.1, -0.1, -0.1]]]), + units=unit.kilojoule_per_mole / unit.nanometer, + ) + assert forces.value.shape == (1, 2, 3) + assert forces.units == unit.kilojoule_per_mole / unit.nanometer + assert forces.classification == "per_atom" + assert forces.name == "forces" + assert forces.property_type == "force" + + with pytest.raises(ValueError): + forces = Forces( + value=np.array([[0.1, 0.1, 0.1, 0.1], [-0.1, -0.1, -0.1, 0.1]]), + units=unit.kilojoule_per_mole / unit.nanometer, + ) + with pytest.raises(ValueError): + forces = Forces( + value=np.array([[0.1, 0.1, 0.1], [-0.1, -0.1, -0.1]]), + units=unit.kilojoule_per_mole / unit.nanometer, + ) + with pytest.raises(ValueError): + forces = Forces( + value=np.array([0.1, 0.1, 0.1, -0.1, -0.1, -0.1]), + units=unit.kilojoule_per_mole / unit.nanometer, + ) + + +def test_initialize_total_charge(): + total_charge = TotalCharge(value=np.array([[0.1]]), units=unit.elementary_charge) + assert total_charge.value.shape == (1, 1) + assert total_charge.units == unit.elementary_charge + assert total_charge.classification == "per_system" + assert total_charge.name == "total_charge" + assert total_charge.property_type == "charge" + + with pytest.raises(ValueError): + total_charge = TotalCharge(value=np.array([0.1]), units=unit.elementary_charge) + with pytest.raises(ValueError): + total_charge = TotalCharge( + value=np.array([[0.1, 0.1]]), units=unit.elementary_charge + ) + with pytest.raises(ValueError): + total_charge = TotalCharge( + value=np.array([[[0.1, 0.1, 0.1]]]), units=unit.elementary_charge + ) + + +def test_initialize_dipole_moment_per_system(): + dipole_moment = DipoleMomentPerSystem( + value=np.array([[0.1, 0.1, 0.1]]), units=unit.debye + ) + assert dipole_moment.value.shape == (1, 3) + assert dipole_moment.units == unit.debye + assert dipole_moment.classification == "per_system" + assert dipole_moment.name == "dipole_moment_per_system" + assert dipole_moment.property_type == "dipole_moment" + + with pytest.raises(ValueError): + dipole_moment = DipoleMomentPerSystem( + value=np.array([0.1, 0.1, 0.1]), units=unit.debye + ) + with pytest.raises(ValueError): + dipole_moment = DipoleMomentPerSystem( + value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), units=unit.debye + ) + + +def test_initialize_quadruople_moment_per_system(): + quadrupole_moment = QuadrupoleMomentPerSystem( + value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), + units=unit.debye * unit.nanometer, + ) + assert quadrupole_moment.value.shape == (1, 3, 3) + assert quadrupole_moment.units == unit.debye * unit.nanometer + assert quadrupole_moment.classification == "per_system" + assert quadrupole_moment.name == "quadrupole_moment_per_system" + assert quadrupole_moment.property_type == "quadrupole_moment" + + +def test_initialize_dipole_moment_scalar_per_system(): + dipole_moment_scalar = DipoleMomentScalarPerSystem( + value=np.array([[0.1]]), units=unit.debye + ) + assert dipole_moment_scalar.value.shape == (1, 1) + assert dipole_moment_scalar.units == unit.debye + assert dipole_moment_scalar.classification == "per_system" + assert dipole_moment_scalar.name == "dipole_moment_scalar_per_system" + assert dipole_moment_scalar.property_type == "dipole_moment" + + +def test_initialize_dipole_moment_per_atom(): + dipole_moment_per_atom = DipoleMomentPerAtom( + value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), units=unit.debye + ) + assert dipole_moment_per_atom.value.shape == (1, 2, 3) + assert dipole_moment_per_atom.units == unit.debye + assert dipole_moment_per_atom.classification == "per_atom" + assert dipole_moment_per_atom.name == "dipole_moment_per_atom" + assert dipole_moment_per_atom.property_type == "dipole_moment" + assert dipole_moment_per_atom.n_atoms == 2 + assert dipole_moment_per_atom.n_configs == 1 + + +def test_initialize_quadrupole_moment_per_atom(): + quadrupole_moment_per_atom = QuadrupoleMomentPerAtom( + value=np.array( + [ + [ + [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], + [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], + ] + ] + ), + units=unit.debye * unit.nanometer, + ) + assert quadrupole_moment_per_atom.value.shape == (1, 2, 3, 3) + assert quadrupole_moment_per_atom.units == unit.debye * unit.nanometer + assert quadrupole_moment_per_atom.classification == "per_atom" + assert quadrupole_moment_per_atom.name == "quadrupole_moment_per_atom" + assert quadrupole_moment_per_atom.property_type == "quadrupole_moment" + assert quadrupole_moment_per_atom.n_atoms == 2 + assert quadrupole_moment_per_atom.n_configs == 1 + + +def test_initialize_octupole_moment_per_atom(): + # octupole moment has value of shape (M, N, 3,3,3) + octupole_moment_per_atom = OctupoleMomentPerAtom( + value=np.array( + [ + [ + [ + [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], + [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], + [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]], + ] + ] + ] + ), + units=unit.debye * unit.nanometer**2, + ) + + assert octupole_moment_per_atom.value.shape == (1, 1, 3, 3, 3) + assert octupole_moment_per_atom.units == unit.debye * unit.nanometer**2 + assert octupole_moment_per_atom.classification == "per_atom" + assert octupole_moment_per_atom.name == "octupole_moment_per_atom" + assert octupole_moment_per_atom.property_type == "octupole_moment" + assert octupole_moment_per_atom.n_atoms == 1 + assert octupole_moment_per_atom.n_configs == 1 + + +def test_initialize_polarizability(): + polarizability = Polarizability( + value=np.array([[0.1]]), + units=unit.bohr**3, + ) + assert polarizability.value.shape == (1, 1) + assert polarizability.units == unit.bohr**3 + assert polarizability.classification == "per_system" + assert polarizability.name == "polarizability" + assert polarizability.property_type == "polarizability" + + with pytest.raises(ValueError): + polarizability = Polarizability( + value=np.array([0.1]), + units=unit.bohr**3, + ) + with pytest.raises(ValueError): + polarizability = Polarizability( + value=np.array([[0.1, 0.1]]), + units=unit.bohr**3, + ) + with pytest.raises(ValueError): + polarizability = Polarizability( + value=np.array([[[0.1]]]), + units=unit.bohr**3, + ) + + +def test_initialize_base_model(): + base_prop = PropertyBaseModel( + name="test_prop", + value=np.array([[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]]), + units=unit.nanometer, + property_type="length", + classification="per_atom", + ) + assert base_prop.value.shape == (1, 3, 3) + assert base_prop.units == unit.nanometer + assert base_prop.classification == "per_atom" + assert base_prop.name == "test_prop" + assert base_prop.property_type == "length" + + +def test_initialize_bond_orders(): + bond_orders = BondOrders(value=np.array([[[1, 2, 3], [1, 2, 3], [1, 2, 3]]])) + assert bond_orders.value.shape == (1, 3, 3) + + with pytest.raises(ValueError): + bond_orders = BondOrders(value=np.array([1, 2, 3])) + with pytest.raises(ValueError): + bond_orders = BondOrders(value=np.array([[1, 2, 3]])) + with pytest.raises(ValueError): + bond_orders = BondOrders(value=np.array([[[1, 2, 3]]])) + with pytest.raises(ValueError): + bond_orders = BondOrders(value=np.array([[[1, 2, 3], [1, 2, 3]]])) diff --git a/modelforge-curate/modelforge/curate/tests/test_units.py b/modelforge-curate/modelforge/curate/tests/test_units.py new file mode 100644 index 00000000..3e98c2a0 --- /dev/null +++ b/modelforge-curate/modelforge/curate/tests/test_units.py @@ -0,0 +1,109 @@ +import pytest +import numpy as np +from openff.units import unit + +from modelforge.curate import Record, SourceDataset +from modelforge.curate.units import GlobalUnitSystem +from modelforge.curate.properties import * + + +def test_units_representation(capsys): + print(GlobalUnitSystem()) + + out, _ = capsys.readouterr() + + assert "area : nanometer ** 2" in out + assert "atomic_numbers : dimensionless" in out + assert "charge : elementary_charge" in out + assert "dimensionless : dimensionless" in out + assert "dipole_moment : elementary_charge * nanometer" in out + assert "energy : kilojoule_per_mole" in out + assert "force : kilojoule_per_mole / nanometer" in out + assert "frequency : gigahertz" in out + assert "heat_capacity : kilojoule_per_mole / kelvin" in out + assert "length : nanometer" in out + assert "octupole_moment : elementary_charge * nanometer ** 3" in out + assert "polarizability : nanometer ** 3" in out + assert "quadrupole_moment : elementary_charge * nanometer ** 2" in out + assert "wavenumber : 1 / centimeter" in out + + assert "get_units" not in out + assert "set_global_units" not in out + + # add in a unit and assert it shows up in the print + GlobalUnitSystem.set_global_units("pressure", unit.bar) + print(GlobalUnitSystem()) + out, _ = capsys.readouterr() + assert "pressure : bar" in out + + +def test_default_unit_system(): + + assert GlobalUnitSystem.name == "default" + assert GlobalUnitSystem.length == unit.nanometer + assert GlobalUnitSystem.force == unit.kilojoule_per_mole / unit.nanometer + assert GlobalUnitSystem.energy == unit.kilojoule_per_mole + assert GlobalUnitSystem.charge == unit.elementary_charge + assert GlobalUnitSystem.dipole_moment == unit.elementary_charge * unit.nanometer + assert ( + GlobalUnitSystem.quadrupole_moment == unit.elementary_charge * unit.nanometer**2 + ) + assert GlobalUnitSystem.polarizability == unit.nanometer**3 + assert GlobalUnitSystem.atomic_numbers == unit.dimensionless + assert GlobalUnitSystem.dimensionless == unit.dimensionless + + assert GlobalUnitSystem.get_units("length") == unit.nanometer + assert ( + GlobalUnitSystem.get_units("force") == unit.kilojoule_per_mole / unit.nanometer + ) + assert GlobalUnitSystem.get_units("energy") == unit.kilojoule_per_mole + assert GlobalUnitSystem.get_units("charge") == unit.elementary_charge + assert ( + GlobalUnitSystem.get_units("dipole_moment") + == unit.elementary_charge * unit.nanometer + ) + assert ( + GlobalUnitSystem.get_units("quadrupole_moment") + == unit.elementary_charge * unit.nanometer**2 + ) + assert GlobalUnitSystem.get_units("polarizability") == unit.nanometer**3 + assert GlobalUnitSystem.get_units("atomic_numbers") == unit.dimensionless + assert GlobalUnitSystem.get_units("dimensionless") == unit.dimensionless + + +def test_set_global_units(): + GlobalUnitSystem.length = unit.angstrom + assert GlobalUnitSystem.length == unit.angstrom + + GlobalUnitSystem.set_global_units("pressure", unit.bar) + assert GlobalUnitSystem.get_units("pressure") == unit.bar + + GlobalUnitSystem.set_global_units("length", unit.nanometer) + assert GlobalUnitSystem.length == unit.nanometer + + GlobalUnitSystem.set_global_units("length", "nanometer") + assert GlobalUnitSystem.length == unit.nanometer + + import pint + + with pytest.raises(pint.errors.UndefinedUnitError): + GlobalUnitSystem.set_global_units("length", "not_a_unit") + + with pytest.raises(ValueError): + GlobalUnitSystem.set_global_units("length", 123.234) + + assert GlobalUnitSystem.length == unit.nanometer + + # test setting a unit that is not in the global unit system + # will produce an error + with pytest.raises(AttributeError): + GlobalUnitSystem.tacos == unit.nanometer + + +def test_conversion(): + from modelforge.curate.utils import _convert_unit_str_to_unit_unit + + output = _convert_unit_str_to_unit_unit("nanometer") + assert output.is_compatible_with(unit.nanometer) + assert output == unit.nanometer + assert isinstance(output, unit.Unit)