diff --git a/dist/js/method.d.ts b/dist/js/method.d.ts index 54f6730..590b1ec 100644 --- a/dist/js/method.d.ts +++ b/dist/js/method.d.ts @@ -1,9 +1,10 @@ import { InMemoryEntity } from "@mat3ra/code/dist/js/entity"; +import { type HashedEntity } from "@mat3ra/code/dist/js/entity/mixins/HashedEntityMixin"; import type { Constructor } from "@mat3ra/code/dist/js/utils/types"; import type { AnyObject } from "@mat3ra/esse/dist/js/esse/types"; import type { BaseMethod, SlugifiedEntry } from "@mat3ra/esse/dist/js/types"; import { type MethodSchemaMixin } from "./generated/MethodSchemaMixin"; -type Base = typeof InMemoryEntity & Constructor; +type Base = typeof InMemoryEntity & Constructor & Constructor; interface MethodData extends Record { searchText?: string; } @@ -20,8 +21,8 @@ export declare class Method extends Method_base implements BaseMethod { get searchText(): string; setSearchText(searchText: string): void; setData(data?: MethodData): void; - get omitInHashCalculation(): boolean; cleanData(fieldsToExclude?: string[]): MethodData; toJSONWithCleanData(fieldsToExclude?: string[]): BaseMethod; + getHashObject(): Record; } export {}; diff --git a/dist/js/method.js b/dist/js/method.js index 3d0b8d2..54c4717 100644 --- a/dist/js/method.js +++ b/dist/js/method.js @@ -1,12 +1,9 @@ "use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); exports.Method = void 0; const entity_1 = require("@mat3ra/code/dist/js/entity"); +const HashedEntityMixin_1 = require("@mat3ra/code/dist/js/entity/mixins/HashedEntityMixin"); const utils_1 = require("@mat3ra/code/dist/js/utils"); -const lodash_1 = __importDefault(require("lodash")); const default_methods_1 = require("./default_methods"); const MethodSchemaMixin_1 = require("./generated/MethodSchemaMixin"); class Method extends entity_1.InMemoryEntity { @@ -34,10 +31,6 @@ class Method extends entity_1.InMemoryEntity { setData(data = {}) { this.setProp("data", data); } - get omitInHashCalculation() { - const data = this.data; - return !(data === null || data === void 0 ? void 0 : data.searchText) && lodash_1.default.isEmpty(lodash_1.default.omit(data, "searchText")); - } cleanData(fieldsToExclude = []) { const filteredData = { ...this.data }; fieldsToExclude.forEach((field) => { @@ -49,6 +42,12 @@ class Method extends entity_1.InMemoryEntity { const json = { ...this._json, data: this.cleanData(fieldsToExclude) }; return (0, utils_1.deepClone)(json); } + getHashObject() { + const json = { ...this.toJSONWithCleanData() }; + delete json.data; + return json; + } } exports.Method = Method; (0, MethodSchemaMixin_1.methodSchemaMixin)(Method.prototype); +(0, HashedEntityMixin_1.hashedEntityMixin)(Method.prototype); diff --git a/dist/js/model.d.ts b/dist/js/model.d.ts index 594a836..bc8f209 100644 --- a/dist/js/model.d.ts +++ b/dist/js/model.d.ts @@ -1,4 +1,5 @@ import { InMemoryEntity } from "@mat3ra/code/dist/js/entity"; +import { type HashedEntity } from "@mat3ra/code/dist/js/entity/mixins/HashedEntityMixin"; import type { Constructor } from "@mat3ra/code/dist/js/utils/types"; import type { AnyObject } from "@mat3ra/esse/dist/js/esse/types"; import type { AnyModelSchema, ApplicationSchema, BaseMethod, BaseModel, SlugifiedEntry, SlugifiedEntryOrSlug } from "@mat3ra/esse/dist/js/types"; @@ -6,7 +7,7 @@ import { type ModelSchemaMixin } from "./generated/ModelSchemaMixin"; import { Method } from "./method"; import { MethodFactory } from "./methods/factory"; import type { MethodTreeBranch, ModelConfig, ModelTree } from "./types"; -type Base = typeof InMemoryEntity & Constructor; +type Base = typeof InMemoryEntity & Constructor & Constructor; declare const Model_base: Base; export declare class Model extends Model_base implements BaseModel { protected _application?: ApplicationSchema; @@ -43,6 +44,6 @@ export declare class Model extends Model_base implements BaseModel { protected _stringToSlugifiedObject(slug: SlugifiedEntryOrSlug): SlugifiedEntry; get isUnknown(): boolean; protected get subtypeSlug(): string; - calculateHash(): string; + getHashObject(): Record; } export {}; diff --git a/dist/js/model.js b/dist/js/model.js index 2d5b3d1..ee622f7 100644 --- a/dist/js/model.js +++ b/dist/js/model.js @@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); exports.Model = void 0; const entity_1 = require("@mat3ra/code/dist/js/entity"); -const utils_1 = require("@mat3ra/utils"); +const HashedEntityMixin_1 = require("@mat3ra/code/dist/js/entity/mixins/HashedEntityMixin"); const lodash_1 = __importDefault(require("lodash")); const default_models_1 = require("./default_models"); const ModelSchemaMixin_1 = require("./generated/ModelSchemaMixin"); @@ -129,14 +129,12 @@ class Model extends entity_1.InMemoryEntity { const subtype = this.subtype; return typeof subtype === "string" ? subtype : subtype.slug; } - calculateHash() { - var _a; + getHashObject() { const json = this.toJSON(); - if (this.Method.omitInHashCalculation) { - (_a = json.method) === null || _a === void 0 ? true : delete _a.data; - } - return utils_1.Utils.hash.calculateHashFromObject(json); + json.method = this.Method.calculateHash(); + return json; } } exports.Model = Model; (0, ModelSchemaMixin_1.modelSchemaMixin)(Model.prototype); +(0, HashedEntityMixin_1.hashedEntityMixin)(Model.prototype); diff --git a/dist/js/tree.d.ts b/dist/js/tree.d.ts index 6ba2d56..0b95d74 100644 --- a/dist/js/tree.d.ts +++ b/dist/js/tree.d.ts @@ -3,47 +3,47 @@ import type { ModelTree } from "./types"; export declare const MODEL_TREE: { dft: { gga: { - refiners: string[]; - modifiers: string[]; + functionals: string[]; methods: { - pseudopotential: string[]; localorbital: string[]; + pseudopotential: string[]; unknown: string[]; }; - functionals: string[]; - }; - lda: { - refiners: string[]; modifiers: string[]; + refiners: string[]; + }; + hybrid: { + functionals: string[]; methods: { - pseudopotential: string[]; localorbital: string[]; + pseudopotential: string[]; unknown: string[]; }; - functionals: string[]; }; - hybrid: { + lda: { + functionals: string[]; methods: { - pseudopotential: string[]; localorbital: string[]; + pseudopotential: string[]; unknown: string[]; }; - functionals: string[]; + modifiers: string[]; + refiners: string[]; }; other: { + functionals: string[]; methods: { - pseudopotential: string[]; localorbital: string[]; + pseudopotential: string[]; unknown: string[]; }; - functionals: string[]; }; }; ml: { re: { methods: { - linear: string[]; kernel_ridge: string[]; + linear: string[]; }; }; }; @@ -56,9 +56,9 @@ export declare const MODEL_TREE: { }; }, MODEL_NAMES: { dft: string; - lda: string; gga: string; hybrid: string; + lda: string; ml: string; re: string; }; diff --git a/package-lock.json b/package-lock.json index 3e197d2..55ef2a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "@mat3ra/code": "2026.5.27-0", "@mat3ra/esse": "2026.5.27-0", "@mat3ra/prode": "2026.5.28-0", - "@mat3ra/standata": "2025.11.12-0", + "@mat3ra/standata": "2026.5.28-0", "@mat3ra/tsconfig": "2024.6.3-0", "@types/chai": "^4.3.11", "@types/mocha": "^10.0.6", @@ -3805,81 +3805,41 @@ } }, "node_modules/@mat3ra/standata": { - "version": "2025.11.12-0", - "resolved": "https://registry.npmjs.org/@mat3ra/standata/-/standata-2025.11.12-0.tgz", - "integrity": "sha512-kZ+0rvJMSYfHTirK5LkaE7D4i/naOCzhKH5h9WfmIzY1+AUvmB0U7P+Diih+atPPgNHVQuY9WP2WGlL2sGUDZA==", + "version": "2026.5.28-0", + "resolved": "https://registry.npmjs.org/@mat3ra/standata/-/standata-2026.5.28-0.tgz", + "integrity": "sha512-jZ3caEHzf02/H0dTDxgePIewPpKihMzY2aYhRuSfp+QWcd8VviLtp46piRsgREuQo1FwSXu3K4yCAVijEklpAw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/cli": "7.16.0", - "@babel/core": "7.24.1", - "@babel/eslint-parser": "7.16.3", - "@babel/plugin-proposal-class-properties": "7.16.0", - "@babel/preset-env": "7.16.4", - "@babel/preset-react": "7.16.7", - "@babel/preset-typescript": "^7.22.5", - "@babel/register": "^7.16.0", - "@babel/runtime-corejs3": "7.16.8", - "cmd-ts": "^0.13.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "ts-node": "^10.9.1", - "typescript": "^4.5.5" + "@types/nunjucks": "^3.2.6", + "@types/sprintf-js": "^1.1.4", + "cmd-ts": "^0.15.0", + "compare-versions": "^6.1.1", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "nunjucks": "^3.2.4", + "sprintf-js": "^1.1.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" }, "engines": { "node": ">=20.0.0" + }, + "peerDependencies": { + "@mat3ra/esse": "*" } }, - "node_modules/@mat3ra/standata/node_modules/@babel/core": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.1.tgz", - "integrity": "sha512-F82udohVyIgGAY2VVj/g34TpFUG606rumIHjTfVbssPg2zTR7PuuEpZcX8JA6sgBfIYmJrFtWgPvHQuJamVqZQ==", + "node_modules/@mat3ra/standata/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.1", - "@babel/parser": "^7.24.1", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "argparse": "^2.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@mat3ra/standata/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@mat3ra/standata/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" + "js-yaml": "bin/js-yaml.js" } }, "node_modules/@mat3ra/tsconfig": { @@ -4251,6 +4211,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nunjucks": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz", + "integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -4259,6 +4226,13 @@ "license": "MIT", "peer": true }, + "node_modules/@types/sprintf-js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/sprintf-js/-/sprintf-js-1.1.4.tgz", + "integrity": "sha512-aWK1reDYWxcjgcIIPmQi3u+OQDuYa9b+lr6eIsGWrekJ9vr1NSjr4Eab8oQ1iKuH1ltFHpXGyerAv1a3FMKxzQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/underscore": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz", @@ -5491,16 +5465,58 @@ } }, "node_modules/cmd-ts": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/cmd-ts/-/cmd-ts-0.13.0.tgz", - "integrity": "sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/cmd-ts/-/cmd-ts-0.15.0.tgz", + "integrity": "sha512-ASyAx+P6DNzNEZ6OYmvjjaLAQeI3L+4lm5rX2Jg8tondXdj06o9JTxi9mfC92KoVbrVFKhZ+j2TfBF8fcura6g==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "debug": "^4.3.4", + "chalk": "^5.4.1", + "debug": "^4.4.1", "didyoumean": "^1.2.2", - "strip-ansi": "^6.0.0" + "strip-ansi": "^7.1.0" + } + }, + "node_modules/cmd-ts/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cmd-ts/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cmd-ts/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/color-convert": { @@ -5553,6 +5569,13 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "license": "MIT" }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/complex.js": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz", @@ -7189,6 +7212,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index c52f674..908af0d 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@mat3ra/code": "2026.5.27-0", "@mat3ra/esse": "2026.5.27-0", "@mat3ra/prode": "2026.5.28-0", - "@mat3ra/standata": "2025.11.12-0", + "@mat3ra/standata": "2026.5.28-0", "@mat3ra/tsconfig": "2024.6.3-0", "@types/chai": "^4.3.11", "@types/mocha": "^10.0.6", diff --git a/src/js/method.ts b/src/js/method.ts index 30cbaff..6543676 100644 --- a/src/js/method.ts +++ b/src/js/method.ts @@ -1,14 +1,17 @@ import { InMemoryEntity } from "@mat3ra/code/dist/js/entity"; +import { + type HashedEntity, + hashedEntityMixin, +} from "@mat3ra/code/dist/js/entity/mixins/HashedEntityMixin"; import { deepClone } from "@mat3ra/code/dist/js/utils"; import type { Constructor } from "@mat3ra/code/dist/js/utils/types"; import type { AnyObject } from "@mat3ra/esse/dist/js/esse/types"; import type { BaseMethod, SlugifiedEntry } from "@mat3ra/esse/dist/js/types"; -import lodash from "lodash"; import { PseudopotentialMethodConfig } from "./default_methods"; import { type MethodSchemaMixin, methodSchemaMixin } from "./generated/MethodSchemaMixin"; -type Base = typeof InMemoryEntity & Constructor; +type Base = typeof InMemoryEntity & Constructor & Constructor; interface MethodData extends Record { searchText?: string; @@ -48,11 +51,6 @@ export class Method extends (InMemoryEntity as Base) implements BaseMethod { this.setProp("data", data); } - get omitInHashCalculation(): boolean { - const data = this.data as MethodData; - return !data?.searchText && lodash.isEmpty(lodash.omit(data, "searchText")); - } - cleanData(fieldsToExclude: string[] = []): MethodData { const filteredData = { ...(this.data as MethodData) }; fieldsToExclude.forEach((field) => { @@ -65,6 +63,13 @@ export class Method extends (InMemoryEntity as Base) implements BaseMethod { const json = { ...this._json, data: this.cleanData(fieldsToExclude) }; return deepClone(json); } + + getHashObject(): Record { + const json = { ...this.toJSONWithCleanData() } as Record; + delete json.data; + return json; + } } methodSchemaMixin(Method.prototype); +hashedEntityMixin(Method.prototype); diff --git a/src/js/model.ts b/src/js/model.ts index 9c4b943..fc14093 100644 --- a/src/js/model.ts +++ b/src/js/model.ts @@ -1,4 +1,8 @@ import { InMemoryEntity } from "@mat3ra/code/dist/js/entity"; +import { + type HashedEntity, + hashedEntityMixin, +} from "@mat3ra/code/dist/js/entity/mixins/HashedEntityMixin"; import type { Constructor } from "@mat3ra/code/dist/js/utils/types"; import type { AnyObject } from "@mat3ra/esse/dist/js/esse/types"; import type { @@ -9,7 +13,6 @@ import type { SlugifiedEntry, SlugifiedEntryOrSlug, } from "@mat3ra/esse/dist/js/types"; -import { Utils } from "@mat3ra/utils"; import lodash from "lodash"; import { DFTModelConfig } from "./default_models"; @@ -21,7 +24,7 @@ import type { MethodTreeBranch, ModelConfig, ModelTree } from "./types"; const EMPTY_BRANCH: MethodTreeBranch = { methods: {} }; -type Base = typeof InMemoryEntity & Constructor; +type Base = typeof InMemoryEntity & Constructor & Constructor; export class Model extends (InMemoryEntity as Base) implements BaseModel { protected _application?: ApplicationSchema; @@ -162,15 +165,12 @@ export class Model extends (InMemoryEntity as Base) implements BaseModel { return typeof subtype === "string" ? subtype : subtype.slug; } - calculateHash(): string { - const json = this.toJSON() as Record & { - method?: { data?: unknown }; - }; - if (this.Method.omitInHashCalculation) { - delete json.method?.data; - } - return Utils.hash.calculateHashFromObject(json as Record); + getHashObject(): Record { + const json = this.toJSON() as Record; + json.method = this.Method.calculateHash(); + return json; } } modelSchemaMixin(Model.prototype); +hashedEntityMixin(Model.prototype); diff --git a/src/py/mat3ra/mode/method.py b/src/py/mat3ra/mode/method.py index 9a8fa10..66a4aac 100644 --- a/src/py/mat3ra/mode/method.py +++ b/src/py/mat3ra/mode/method.py @@ -1,54 +1,34 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from mat3ra.code.entity import InMemoryEntityPydantic from mat3ra.code.mixins import HashedEntityMixin -from mat3ra.esse.models.method import BaseMethod +from mat3ra.esse.models.method import BaseMethod, Data from pydantic import Field +class MethodData(Data, InMemoryEntityPydantic): + searchText: Optional[str] = Field(default=None, exclude_if=lambda v: v is None) + class Method(BaseMethod, HashedEntityMixin, InMemoryEntityPydantic): type: str = Field(default="unknown") subtype: str = Field(default="unknown") - data: Dict[str, Any] = Field(default_factory=dict) + data: MethodData = Field(default_factory=MethodData) def clone_without_data(self) -> "Method": cloned = self.clone() - cloned.data = {} + cloned.data = MethodData() return cloned @classmethod def clean(cls, config: Dict[str, Any]) -> Dict[str, Any]: - data = config.get("data", {}) + raw_data = config.get("data",{}) cleaned = super().clean(config) - cleaned["data"] = data + cleaned["data"] = MethodData(**raw_data).model_dump() return cleaned @property def search_text(self) -> str: - return self.data.get("searchText", "") - - @property - def omit_in_hash_calculation(self) -> bool: - data = self.data - if not data: - return True - # Omit if only searchText is present and empty, or no fields at all - search_text = data.get("searchText", "") - other_fields = {k: v for k, v in data.items() if k != "searchText"} - return not search_text and not other_fields + return self.data.searchText or "" def get_hash_object(self) -> Dict[str, Any]: return self.to_dict(exclude=["data"]) - - def to_dict(self, exclude: Optional[List[str]] = None) -> Dict[str, Any]: - exclude_set = set(exclude) if exclude else set() - should_exclude_data = "data" in exclude_set - exclude_set = {x for x in exclude_set if x != "data"} - - dict_data = super().to_dict(exclude=list(exclude_set) if exclude_set else None) - dict_data = {k: v for k, v in dict_data.items() if v is not None} - - if not should_exclude_data: - dict_data["data"] = self.data.copy() - - return dict_data diff --git a/src/py/mat3ra/mode/model.py b/src/py/mat3ra/mode/model.py index c5c6c40..87a408b 100644 --- a/src/py/mat3ra/mode/model.py +++ b/src/py/mat3ra/mode/model.py @@ -1,12 +1,13 @@ from typing import Any, Dict, Optional from mat3ra.code.entity import InMemoryEntityPydantic +from mat3ra.code.mixins import HashedEntityMixin from mat3ra.esse.models.model import BaseModelModel from pydantic import Field from .method import Method from .methods.factory import MethodFactory -from mat3ra.code.mixins import HashedEntityMixin + class Model(BaseModelModel, HashedEntityMixin, InMemoryEntityPydantic): method: Method = Field(default_factory=lambda: MethodFactory.create({})) @@ -39,5 +40,5 @@ def is_unknown(self) -> bool: def get_hash_object(self) -> Dict[str, Any]: cfg = self.to_dict() - cfg.method = self.method.calculate_hash() + cfg["method"] = self.method.calculate_hash() return cfg diff --git a/src/py/mat3ra/mode/models/dft.py b/src/py/mat3ra/mode/models/dft.py index 7291f54..844ec5c 100644 --- a/src/py/mat3ra/mode/models/dft.py +++ b/src/py/mat3ra/mode/models/dft.py @@ -10,13 +10,9 @@ class DFTModel(Model): type: str = Field(default="dft") subtype: str = Field(default="gga") - functional: Union[Functional7, SlugifiedEntry, Dict[str, Any], None] = Field( + functional: Union[str, Functional7, SlugifiedEntry, Dict[str, Any], None] = Field( default=Functional7.pbe ) refiners: List[Union[SlugifiedEntry, str]] = Field(default_factory=list) modifiers: List[Union[SlugifiedEntry, str]] = Field(default_factory=list) - def __convert_kwargs__(self, **kwargs: Any) -> Dict[str, Any]: - if isinstance(kwargs.get("functional"), str): - kwargs["functional"] = {"slug": kwargs["functional"]} - return super().__convert_kwargs__(**kwargs) diff --git a/tests/fixtures/model_hash.json b/tests/fixtures/model_hash.json index ec3e44e..5a24522 100644 --- a/tests/fixtures/model_hash.json +++ b/tests/fixtures/model_hash.json @@ -3,5 +3,5 @@ "application": "espresso", "workflow": "band_gap" }, - "hash": "39f527f9089b504b529f4a0646b454bf" + "hash": "4c02178c2ed256d0585d363e069be436" } diff --git a/tests/js/model.test.ts b/tests/js/model.test.ts index 17c66d1..ca782d7 100644 --- a/tests/js/model.test.ts +++ b/tests/js/model.test.ts @@ -5,8 +5,11 @@ import { resolve } from "path"; import { Model } from "../../src/js/model"; import { DFTModel } from "../../src/js/models/dft"; +import { ModelFactory } from "../../src/js/models/factory"; import { ModelConfig } from "../../src/js/types"; +const BAND_GAP_WORKFLOW_NAME = "Band Gap"; + describe("Model", () => { // @ts-ignore const obj: ModelConfig = { type: "dft" }; @@ -80,11 +83,13 @@ describe("Model", () => { readFileSync(resolve(__dirname, "../fixtures/model_hash.json"), "utf-8"), ); const standata = new WorkflowStandata(); - const [wfConfig] = standata.findEntitiesByTags( + const workflows = standata.findEntitiesByTags( fixture.standata.application, fixture.standata.workflow, ); - const model = new Model(wfConfig.subworkflows[0].model); + const wfConfig = workflows.find((wf) => wf.name === BAND_GAP_WORKFLOW_NAME); + expect(wfConfig).to.exist; + const model = ModelFactory.create(wfConfig!.subworkflows[0].model); expect(model.calculateHash()).to.equal(fixture.hash); }); }); diff --git a/tests/py/unit/test_dft_model.py b/tests/py/unit/test_dft_model.py index b794dd8..321adca 100644 --- a/tests/py/unit/test_dft_model.py +++ b/tests/py/unit/test_dft_model.py @@ -1,13 +1,13 @@ import pytest from mat3ra.mode import DFTModel, Method -# Skip all tests in this file -pytestmark = pytest.mark.skip(reason="Module not ready") DFT_GGA_CONFIG = {"type": "dft", "subtype": "gga"} DFT_GGA_WITH_FUNCTIONAL = {**DFT_GGA_CONFIG, "functional": "pbe"} PSEUDOPOTENTIAL_NC_METHOD = {"type": "pseudopotential", "subtype": "nc"} PSEUDOPOTENTIAL_US_METHOD = {"type": "pseudopotential", "subtype": "us"} +FUNCTIONAL_PBE_STRING_HASH = "4c02178c2ed256d0585d363e069be436" +FUNCTIONAL_PBE_SLUG_HASH = "cc5abffec41bfb3ceb610090e60be566" TEST_REFINERS = ["hse"] TEST_MODIFIERS = ["soc"] @@ -68,4 +68,22 @@ def test_to_dict_includes_functional(): assert "functional" in json_data +@pytest.mark.parametrize( + "functional,expected_hash", + [ + ("pbe", FUNCTIONAL_PBE_STRING_HASH), + ({"slug": "pbe"}, FUNCTIONAL_PBE_SLUG_HASH), + ], +) +def test_calculate_hash_preserves_functional_shape(functional, expected_hash): + config = { + **DFT_GGA_WITH_FUNCTIONAL, + "functional": functional, + "method": PSEUDOPOTENTIAL_US_METHOD, + } + dft_model = DFTModel.create(config) + + assert dft_model.calculate_hash() == expected_hash + + diff --git a/tests/py/unit/test_method.py b/tests/py/unit/test_method.py index 1fe01b2..8aaf90a 100644 --- a/tests/py/unit/test_method.py +++ b/tests/py/unit/test_method.py @@ -16,34 +16,8 @@ def test_can_be_created(config): method = Method.create(config) assert method.type == config["type"] assert method.subtype == config["subtype"] - - -@pytest.mark.parametrize("config", TEST_CONFIGS) -def test_type_property(config): - method = Method.create(config) - type_value = method.type - - assert isinstance(type_value, str) - assert type_value == config["type"] - - -@pytest.mark.parametrize("config", TEST_CONFIGS) -def test_subtype_property(config): - method = Method.create(config) - subtype_value = method.subtype - - assert subtype_value is not None - assert subtype_value == config["subtype"] - - -@pytest.mark.parametrize("config", TEST_CONFIGS) -def test_data_property(config): - config_with_data = {**config, "data": TEST_DATA} - method = Method.create(config_with_data) - - data = method.data - assert isinstance(data, dict) - assert data["key"] == "value" + assert isinstance(method.type, str) + assert isinstance(method.subtype, str) @pytest.mark.parametrize("config", TEST_CONFIGS) @@ -54,14 +28,6 @@ def test_set_data(config): assert method.data["test"] == "data" -@pytest.mark.parametrize("config", TEST_CONFIGS) -def test_set_search_text(config): - method = Method.create(config) - method.data = TEST_DATA_WITH_SEARCH_TEXT - - assert method.search_text == "test search" - - @pytest.mark.parametrize("config", TEST_CONFIGS) def test_to_json(config): config_with_data = {**config, "data": TEST_DATA} @@ -80,4 +46,4 @@ def test_clone_without_data(config): cloned = method.clone_without_data() assert cloned.type == method.type - assert cloned.data == {} + assert cloned.data.model_dump() == {} diff --git a/tests/py/unit/test_model.py b/tests/py/unit/test_model.py index 67df909..1fbdc2c 100644 --- a/tests/py/unit/test_model.py +++ b/tests/py/unit/test_model.py @@ -2,11 +2,10 @@ from pathlib import Path import pytest +from mat3ra.mode import ModelFactory from mat3ra.standata.workflows import WorkflowStandata -from mat3ra.mode import Method, Model - -pytestmark = pytest.mark.skip(reason="Module not ready") +BAND_GAP_WORKFLOW_NAME = "Band Gap" DFT_GGA_CONFIG = {"type": "dft", "subtype": "gga"} ML_RE_CONFIG = {"type": "ml", "subtype": "re"} @@ -21,39 +20,21 @@ @pytest.mark.parametrize("config", MODEL_CONFIGS) def test_can_be_created(config): - model = Model.create(config) + model = ModelFactory.create(config) assert model.type == config["type"] assert model.subtype == config["subtype"] -@pytest.mark.parametrize("config", MODEL_CONFIGS) -def test_type_property(config): - model = Model.create(config) - type_value = model.type - - assert isinstance(type_value, str) - assert type_value == config["type"] - - -@pytest.mark.parametrize("config", MODEL_CONFIGS) -def test_subtype_property(config): - model = Model.create(config) - subtype_value = model.subtype - - assert subtype_value is not None - assert subtype_value == config["subtype"] - - @pytest.mark.parametrize("config", MODEL_CONFIGS) @pytest.mark.parametrize("method_config", METHOD_CONFIGS) def test_method_property_returns_method_instance(config, method_config): config_with_method = {**config, "method": method_config} - model = Model.create(config_with_method) + model = ModelFactory.create(config_with_method) method_value = model.method assert method_value is not None - assert isinstance(method_value, Method) + assert isinstance(method_value, type(model.method)) assert hasattr(method_value, "data") assert hasattr(method_value, "search_text") @@ -63,7 +44,7 @@ def test_method_property_returns_method_instance(config, method_config): @pytest.mark.parametrize("method_config", METHOD_CONFIGS) def test_to_json(config, method_config): config_with_method = {**config, "method": method_config} - model = Model.create(config_with_method) + model = ModelFactory.create(config_with_method) json_data = model.to_dict() assert json_data["type"] == config["type"] @@ -77,6 +58,7 @@ def test_calculate_hash_matches_fixture(): fixture = json.loads(fixture_path.read_text()) st = fixture["standata"] - wf_config = WorkflowStandata.get_by_name_and_categories(st["workflow"], st["application"]) - model = Model.create(wf_config["subworkflows"][0]["model"]) + workflows = WorkflowStandata.get_by_categories(st["application"], st["workflow"]) + wf_config = next(w for w in workflows if w["name"] == BAND_GAP_WORKFLOW_NAME) + model = ModelFactory.create(wf_config["subworkflows"][0]["model"]) assert model.calculate_hash() == fixture["hash"]