diff --git a/src/context/context.js b/src/context/context.js new file mode 100644 index 0000000..dda2cf0 --- /dev/null +++ b/src/context/context.js @@ -0,0 +1,23 @@ +import { BoundaryConditionsFormDataProvider } from "./providers/BoundaryConditionsFormDataProvider"; +import { MLSettingsContextProvider } from "./providers/MLSettingsContextProvider"; +import { MLTrainTestSplitContextProvider } from "./providers/MLTrainTestSplitContextProvider"; +import { NEBFormDataProvider } from "./providers/NEBFormDataProvider"; +import { PlanewaveCutoffsContextProvider } from "./providers/PlanewaveCutoffsContextProvider"; +import { PointsGridFormDataProvider } from "./providers/PointsGridFormDataProvider"; +import { + ExplicitPointsPath2PIBAFormDataProvider, + ExplicitPointsPathFormDataProvider, + PointsPathFormDataProvider, +} from "./providers/PointsPathFormDataProvider"; + +export default { + BoundaryConditionsFormDataProvider, + MLSettingsContextProvider, + MLTrainTestSplitContextProvider, + NEBFormDataProvider, + PlanewaveCutoffsContextProvider, + PointsGridFormDataProvider, + PointsPathFormDataProvider, + ExplicitPointsPathFormDataProvider, + ExplicitPointsPath2PIBAFormDataProvider, +}; diff --git a/src/context/providers.js b/src/context/providers.js index c016d01..6f7110d 100644 --- a/src/context/providers.js +++ b/src/context/providers.js @@ -1,4 +1,4 @@ -import { context } from "@exabyte-io/mode.js"; +import context from "./context"; const { BoundaryConditionsFormDataProvider, diff --git a/src/context/providers/BoundaryConditionsFormDataProvider.js b/src/context/providers/BoundaryConditionsFormDataProvider.js new file mode 100644 index 0000000..e34cfeb --- /dev/null +++ b/src/context/providers/BoundaryConditionsFormDataProvider.js @@ -0,0 +1,76 @@ +import { JSONSchemaFormDataProvider, MaterialContextMixin } from "@exabyte-io/code.js/dist/context"; +import { deepClone } from "@exabyte-io/code.js/dist/utils"; +import { Made } from "@exabyte-io/made.js"; +import { mix } from "mixwith"; + +export class BoundaryConditionsFormDataProvider extends mix(JSONSchemaFormDataProvider).with( + MaterialContextMixin, +) { + static Material = Made.Material; + + get boundaryConditions() { + return this.material.metadata.boundaryConditions || {}; + } + + // eslint-disable-next-line class-methods-use-this + get defaultData() { + return { + type: this.boundaryConditions.type || "pbc", + offset: this.boundaryConditions.offset || 0, + electricField: 0, + targetFermiEnergy: 0, + }; + } + + // eslint-disable-next-line class-methods-use-this + get uiSchema() { + return { + type: { "ui:disabled": true }, + offset: { "ui:disabled": true }, + electricField: {}, + targetFermiEnergy: {}, + }; + } + + // eslint-disable-next-line class-methods-use-this + get humanName() { + return "Boundary Conditions"; + } + + yieldDataForRendering() { + const data = deepClone(this.yieldData()); + data.boundaryConditions.offset *= Made.coefficients.ANGSTROM_TO_BOHR; + data.boundaryConditions.targetFermiEnergy *= Made.coefficients.EV_TO_RY; + data.boundaryConditions.electricField *= Made.coefficients.EV_A_TO_RY_BOHR; + return data; + } + + get jsonSchema() { + return { + $schema: "http://json-schema.org/draft-04/schema#", + type: "object", + properties: { + type: { + type: "string", + title: "Type", + default: this.defaultData.type, + }, + offset: { + type: "number", + title: "Offset (A)", + default: this.defaultData.offset, + }, + electricField: { + type: "number", + title: "Electric Field (eV/A)", + default: this.defaultData.electricField, + }, + targetFermiEnergy: { + type: "number", + title: "Target Fermi Energy (eV)", + default: this.defaultData.targetFermiEnergy, + }, + }, + }; + } +} diff --git a/src/context/providers/MLSettingsContextProvider.js b/src/context/providers/MLSettingsContextProvider.js new file mode 100644 index 0000000..2b3724d --- /dev/null +++ b/src/context/providers/MLSettingsContextProvider.js @@ -0,0 +1,43 @@ +import { Application } from "@exabyte-io/ade.js"; +import { ApplicationContextMixin, ContextProvider } from "@exabyte-io/code.js/dist/context"; +import { mix } from "mixwith"; + +export class MLSettingsContextProvider extends mix(ContextProvider).with(ApplicationContextMixin) { + static Application = Application; + + // eslint-disable-next-line class-methods-use-this + get uiSchema() { + return { + target_column_name: {}, + problem_category: {}, + }; + } + + // eslint-disable-next-line class-methods-use-this + get defaultData() { + return { + target_column_name: "target", + problem_category: "regression", + }; + } + + get jsonSchema() { + return { + $schema: "http://json-schema.org/draft-04/schema#", + title: " ", + description: "Settings important to machine learning runs.", + type: "object", + properties: { + target_column_name: { + type: "string", + default: this.defaultData.target_column_name, + }, + problem_category: { + type: "string", + default: this.defaultData.problem_category, + enum: ["regression", "classification", "clustering"], + }, + }, + }; + } +} diff --git a/src/context/providers/MLTrainTestSplitContextProvider.js b/src/context/providers/MLTrainTestSplitContextProvider.js new file mode 100644 index 0000000..978faaa --- /dev/null +++ b/src/context/providers/MLTrainTestSplitContextProvider.js @@ -0,0 +1,42 @@ +import { Application } from "@exabyte-io/ade.js"; +import { ApplicationContextMixin, ContextProvider } from "@exabyte-io/code.js/dist/context"; +import { mix } from "mixwith"; + +export class MLTrainTestSplitContextProvider extends mix(ContextProvider).with( + ApplicationContextMixin, +) { + static Application = Application; + + // eslint-disable-next-line class-methods-use-this + get uiSchema() { + return { + target_column_name: {}, + problem_category: {}, + }; + } + + // eslint-disable-next-line class-methods-use-this + get defaultData() { + return { + fraction_held_as_test_set: 0.2, + }; + } + + get jsonSchema() { + return { + $schema: "http://json-schema.org/draft-04/schema#", + title: " ", + description: + "Fraction held as the test set. For example, a value of 0.2 corresponds to an 80/20 train/test split.", + type: "object", + properties: { + fraction_held_as_test_set: { + type: "number", + default: this.defaultData.fraction_held_as_test_set, + minimum: 0, + maximum: 1, + }, + }, + }; + } +} diff --git a/src/context/providers/NEBFormDataProvider.js b/src/context/providers/NEBFormDataProvider.js new file mode 100644 index 0000000..f90ea01 --- /dev/null +++ b/src/context/providers/NEBFormDataProvider.js @@ -0,0 +1,32 @@ +import { JSONSchemaFormDataProvider } from "@exabyte-io/code.js/dist/context"; + +export class NEBFormDataProvider extends JSONSchemaFormDataProvider { + // eslint-disable-next-line class-methods-use-this + get defaultData() { + return { + nImages: 1, + }; + } + + // eslint-disable-next-line class-methods-use-this + get uiSchema() { + return { + nImages: {}, + }; + } + + get jsonSchema() { + return { + $schema: "http://json-schema.org/draft-04/schema#", + title: " ", + description: "Number of intermediate NEB images.", + type: "object", + properties: { + nImages: { + type: "number", + default: this.defaultData.nImages, + }, + }, + }; + } +} diff --git a/src/context/providers/PlanewaveCutoffsContextProvider.js b/src/context/providers/PlanewaveCutoffsContextProvider.js new file mode 100644 index 0000000..ba7dae0 --- /dev/null +++ b/src/context/providers/PlanewaveCutoffsContextProvider.js @@ -0,0 +1,65 @@ +import { Application } from "@exabyte-io/ade.js"; +import { ApplicationContextMixin, ContextProvider } from "@exabyte-io/code.js/dist/context"; +import { mix } from "mixwith"; + +const cutoffConfig = { + vasp: {}, // assuming default cutoffs for VASP + espresso: { + // assuming the default GBRV set of pseudopotentials is used + wavefunction: 40, + density: 200, + }, +}; + +export class PlanewaveCutoffsContextProvider extends mix(ContextProvider).with( + ApplicationContextMixin, +) { + static Application = Application; + + // eslint-disable-next-line class-methods-use-this + get uiSchema() { + return { + wavefunction: {}, + density: {}, + }; + } + + get defaultData() { + return { + wavefunction: this.defaultECUTWFC, + density: this.defaultECUTRHO, + }; + } + + get _cutoffConfigPerApplication() { + return cutoffConfig[this.application.name]; + } + + get defaultECUTWFC() { + return this._cutoffConfigPerApplication.wavefunction || null; + } + + get defaultECUTRHO() { + return this._cutoffConfigPerApplication.density || null; + } + + get jsonSchema() { + return { + $schema: "http://json-schema.org/draft-04/schema#", + title: " ", + description: + "Planewave cutoff parameters for electronic wavefunctions and density. Units are specific to simulation engine.", + type: "object", + properties: { + wavefunction: { + type: "number", + default: this.defaultECUTWFC, + }, + density: { + type: "number", + default: this.defaultECUTRHO, + }, + }, + }; + } +} diff --git a/src/context/providers/PointsGridFormDataProvider.js b/src/context/providers/PointsGridFormDataProvider.js new file mode 100644 index 0000000..76ad40c --- /dev/null +++ b/src/context/providers/PointsGridFormDataProvider.js @@ -0,0 +1,168 @@ +import { JSONSchemaFormDataProvider, MaterialContextMixin } from "@exabyte-io/code.js/dist/context"; +import { Made } from "@exabyte-io/made.js"; +import lodash from "lodash"; +import { mix } from "mixwith"; +// TODO : pass appSettings to use defaultKPPRA + +export class PointsGridFormDataProvider extends mix(JSONSchemaFormDataProvider).with( + MaterialContextMixin, +) { + static Material = Made.Material; + + constructor(config) { + super(config); + this._divisor = config.divisor || 1; // KPPRA will be divided by this number + + this.dimensions = lodash.get(this.data, "dimensions") || this._defaultDimensions; + this.shifts = lodash.get(this.data, "shifts") || this._defaultShifts; + + // init class fields from data (as constructed from context in parent) + this.KPPRA = lodash.get(this.data, "KPPRA") || this._defaultKPPRA; + this.preferKPPRA = lodash.get(this.data, "preferKPPRA", false); + } + + getDefaultDimension() { + return this._getGridFromKPPRA(this._defaultKPPRA).dimensions[0]; + } + + // eslint-disable-next-line class-methods-use-this + getDefaultShift() { + return 0; + } + + // eslint-disable-next-line class-methods-use-this + get _defaultDimensions() { + return Array(3).fill(this.getDefaultDimension()); + } + + // eslint-disable-next-line class-methods-use-this + get _defaultShifts() { + return Array(3).fill(this.getDefaultShift()); + } + + get _defaultKPPRA() { + return Math.floor(10 / this._divisor); + } + + get jsonSchema() { + const kOrQ = this.name[0]; + const vector = { + type: "array", + items: { + type: "number", + }, + minItems: 3, + maxItems: 3, + }; + + const vector_ = (defaultValue) => { + return { + ...vector, + items: { + type: "number", + default: defaultValue, + }, + }; + }; + + return { + $schema: "http://json-schema.org/draft-04/schema#", + description: `3D grid with shifts. Default min value for ${kOrQ.toUpperCase()}PPRA (${kOrQ}pt per reciprocal atom) is ${ + this._defaultKPPRA + }.`, + type: "object", + properties: { + dimensions: vector_(this.getDefaultDimension()), + shifts: vector_(this.getDefaultShift()), + KPPRA: { + type: "integer", + minimum: 1, + default: this.KPPRA, + }, + preferKPPRA: { + type: "boolean", + default: this.preferKPPRA, + }, + }, + required: ["dimensions", "shifts"], + }; + } + + _arraySubStyle(emptyValue = 0) { + return { + "ui:options": { + addable: false, + orderable: false, + removable: false, + }, + items: { + "ui:disabled": this.preferKPPRA, + // TODO: extract the actual current values from context + "ui:placeholder": "1", + "ui:emptyValue": emptyValue, + }, + }; + } + + get uiSchema() { + return { + dimensions: this._arraySubStyle(1), + shifts: this._arraySubStyle(0), + KPPRA: { + "ui:disabled": !this.preferKPPRA, + "ui:emptyValue": this.KPPRA, + "ui:placeholder": this.KPPRA.toString(), // make string to prevent prop type error + }, + preferKPPRA: { + ...this.fieldStyles("p-t-20"), // add padding top to level with other elements + "ui:emptyValue": true, + }, + }; + } + + get _defaultData() { + return { + dimensions: this._defaultDimensions, + shifts: this._defaultShifts, + KPPRA: this._defaultKPPRA, + preferKPPRA: false, + }; + } + + get _defaultDataWithMaterial() { + // if `data` is present and material is updated, prioritize `data` when `preferKPPRA` is not set + return this.preferKPPRA + ? this._getGridFromKPPRA(this.KPPRA) + : this.data || this._defaultData; + } + + get defaultData() { + return this.material ? this._defaultDataWithMaterial : this._defaultData; + } + + _getGridFromKPPRA(KPPRA) { + const nAtoms = this.material ? this.material.Basis.nAtoms : 1; + const dimension = Math.ceil((KPPRA / nAtoms) ** (1 / 3)); + return { + dimensions: Array(3).fill(dimension), + shifts: this._defaultShifts, + }; + } + + _getKPPRAFromGrid(grid = this.defaultData) { + const nAtoms = this.material ? this.material.Basis.nAtoms : 1; + return grid.dimensions.reduce((a, b) => a * b) * nAtoms; + } + + transformData(data) { + // 1. check if KPPRA is preferred + if (data.preferKPPRA) { + // 2. KPPRA is preferred => recalculate grid; NOTE: `data.KPPRA` is undefined at first + data.dimensions = this._getGridFromKPPRA(data.KPPRA).dimensions; + } else { + // 3. KPPRA is not preferred => grid was => recalculate KPPRA + data.KPPRA = this._getKPPRAFromGrid(data); + } + return data; + } +} diff --git a/src/context/providers/PointsPathFormDataProvider.js b/src/context/providers/PointsPathFormDataProvider.js new file mode 100644 index 0000000..5fc77e4 --- /dev/null +++ b/src/context/providers/PointsPathFormDataProvider.js @@ -0,0 +1,168 @@ +/* eslint-disable max-classes-per-file */ +import { Application } from "@exabyte-io/ade.js"; +import { + ApplicationContextMixin, + JSONSchemaFormDataProvider, + MaterialContextMixin, +} from "@exabyte-io/code.js/dist/context"; +import { math as codeJSMath } from "@exabyte-io/code.js/dist/math"; +import { Made } from "@exabyte-io/made.js"; +import { mix } from "mixwith"; +import s from "underscore.string"; + +const defaultPoint = "Г"; +const defaultSteps = 10; + +export class PointsPathFormDataProvider extends mix(JSONSchemaFormDataProvider).with( + ApplicationContextMixin, + MaterialContextMixin, +) { + static Material = Made.Material; + + static Application = Application; + + constructor(config) { + super(config); + this.reciprocalLattice = new Made.ReciprocalLattice(this.material.lattice); + this.symmetryPoints = this.symmetryPointsFromMaterial; + } + + get isEditedIsSetToFalseOnMaterialUpdate() { + return this.isMaterialUpdated || this.isMaterialCreatedDefault; + } + + get defaultData() { + return this.reciprocalLattice.defaultKpointPath; + } + + get symmetryPointsFromMaterial() { + return this.reciprocalLattice.symmetryPoints; + } + + get jsonSchema() { + // no need to pass context to get symmetry points on client + const points = [].concat(this.symmetryPoints).map((x) => x.point); + return { + $schema: "http://json-schema.org/draft-04/schema#", + title: " ", + description: "path in reciprocal space", + type: "array", + items: { + type: "object", + properties: { + point: { + type: "string", + default: defaultPoint, + enum: points, + }, + steps: { + type: "integer", + default: defaultSteps, + }, + }, + }, + minItems: 1, + }; + } + + // eslint-disable-next-line class-methods-use-this + get uiSchema() { + return { + items: {}, + }; + } + + get uiSchemaStyled() { + return { + items: { + point: this.defaultFieldStyles, + steps: this.defaultFieldStyles, + }, + }; + } + + get fields() { + const hasRequiredFn = typeof this.material.getBrillouinZoneImageComponent === "function"; + if (!hasRequiredFn) { + console.log( + "PointsPathFormDataProvider: Material class has no function" + + " 'getBrillouinZoneImageComponent'! Returning empty Object instead.", + ); + return {}; + } + return { + // eslint-disable-next-line no-unused-vars + TitleField: ({ title, required }) => + this.material.getBrillouinZoneImageComponent(title), + }; + } + + get useExplicitPath() { + return this.application.name === "vasp"; + } + + // override yieldData to avoid storing explicit path in saved context + yieldDataForRendering() { + return this.yieldData(this.useExplicitPath); + } + + transformData(path = [], useExplicitPath = false) { + const rawData = path.map((p) => { + const point = this.symmetryPoints.find((sp) => sp.point === p.point); + return { ...p, coordinates: point.coordinates }; + }); + const processedData = useExplicitPath ? this._convertToExplicitPath(rawData) : rawData; + // make coordinates into string and add formatting + return processedData.map((p) => { + const coordinates = this.is2PIBA + ? this.get2PIBACoordinates(p.coordinates) + : p.coordinates; + p.coordinates = coordinates.map((c) => s.sprintf("%14.9f", c)); + return p; + }); + } + + get2PIBACoordinates(point) { + return this.reciprocalLattice.getCartesianCoordinates(point); + } + + // Initially, path contains symmetry points with steps counts. + // This function explicitly calculates each point between symmetry points by step counts. + // eslint-disable-next-line class-methods-use-this + _convertToExplicitPath(path) { + const points = []; + for (let i = 0; i < path.length - 1; i++) { + const startPoint = path[i]; + const endPoint = path[i + 1]; + const middlePoints = codeJSMath.calculateSegmentsBetweenPoints3D( + startPoint.coordinates, + endPoint.coordinates, + startPoint.steps, + ); + points.push(startPoint.coordinates); + points.push(...middlePoints); + // Include endPoint into path for the last section, otherwise it will be included by next loop iteration + if (path.length - 2 === i) points.push(endPoint.coordinates); + } + return points.map((x) => { + return { + coordinates: x, + steps: 1, + }; + }); + } +} + +export class ExplicitPointsPathFormDataProvider extends PointsPathFormDataProvider { + // eslint-disable-next-line class-methods-use-this + get useExplicitPath() { + return true; + } +} + +export class ExplicitPointsPath2PIBAFormDataProvider extends ExplicitPointsPathFormDataProvider { + // eslint-disable-next-line class-methods-use-this + get is2PIBA() { + return true; + } +}