From 1d1e7d5a29beee8afabfa5687ddc15352e870bef Mon Sep 17 00:00:00 2001 From: Michele Ceriotti Date: Tue, 26 Sep 2023 13:07:07 -0700 Subject: [PATCH] Typeset-side shape validation --- python/chemiscope/__init__.py | 4 +- python/chemiscope/input.py | 4 +- python/chemiscope/structures/__init__.py | 4 +- python/chemiscope/structures/_ase.py | 6 +- python/examples/shapes.py | 4 +- src/dataset.ts | 203 ++++++++++++----------- src/structure/shapes.ts | 9 +- 7 files changed, 119 insertions(+), 115 deletions(-) diff --git a/python/chemiscope/__init__.py b/python/chemiscope/__init__.py index bb8803a8b..d714d8f19 100644 --- a/python/chemiscope/__init__.py +++ b/python/chemiscope/__init__.py @@ -7,8 +7,8 @@ composition_properties, ellipsoid_from_tensor, extract_lammps_shapes_from_ase, - extract_tensors_from_ase, - extract_vectors_from_ase, + ase_vectors_to_arrows, + ase_tensors_to_ellipsoids, extract_properties, librascal_atomic_environments, ) diff --git a/python/chemiscope/input.py b/python/chemiscope/input.py index 8439a4d7c..0e8cf5cc7 100644 --- a/python/chemiscope/input.py +++ b/python/chemiscope/input.py @@ -190,8 +190,8 @@ def create_input( Each of these can contain some or all of the parameters associated with each shape, and the parameters for each shape are obtained by combining the parameters from the - most general to the most specific, i.e., if there is a duplicate key in the `global` and `atom` - fields, the value within the `atom` field will supercede the `global` field for that atom. + most general to the most specific, i.e., if there is a duplicate key in the `global` and `atom` + fields, the value within the `atom` field will supersede the `global` field for that atom. The parameters for atom `k` that is part of structure `j` are obtained as .. code-block:: python diff --git a/python/chemiscope/structures/__init__.py b/python/chemiscope/structures/__init__.py index 7b93f8775..b5aa31110 100644 --- a/python/chemiscope/structures/__init__.py +++ b/python/chemiscope/structures/__init__.py @@ -12,8 +12,8 @@ from ._ase import ( # noqa isort: skip extract_lammps_shapes_from_ase, - extract_tensors_from_ase, - extract_vectors_from_ase, + ase_vectors_to_arrows, + ase_tensors_to_ellipsoids, ) from ._shapes import ( # noqa diff --git a/python/chemiscope/structures/_ase.py b/python/chemiscope/structures/_ase.py index 4d7731182..0b564e424 100644 --- a/python/chemiscope/structures/_ase.py +++ b/python/chemiscope/structures/_ase.py @@ -382,10 +382,10 @@ def ase_vectors_to_arrows(frames, key="forces", target=None, **kwargs): return {"kind": "arrow", "parameters": {"global": globs, "structure": vectors}} -def ase_tensors_to_arrows(frames, key="tensor", target=None, **kwargs): +def ase_tensors_to_ellipsoids(frames, key="tensor", target=None, **kwargs): """ - Extract a 3-tensor atom property from a list of ase.Atoms - objects, and returns a list of arrow shapes. Besides the specific + Extract a 2-tensor atom property from a list of ase.Atoms + objects, and returns a list of ellipsoids shapes. Besides the specific parameters it also accepts the same parameters as `ellipsoid_from_tensor`, which are used to draw the shapes diff --git a/python/examples/shapes.py b/python/examples/shapes.py index 38c945fd0..664c700de 100644 --- a/python/examples/shapes.py +++ b/python/examples/shapes.py @@ -143,7 +143,7 @@ { "position": [3, 2, 1], "color": 0x00FF00, - "orientation": [0.2, 0.4, 0.1, 1], + "orientation": [0.5, -0.5, 0, 1 / np.sqrt(2)], }, ], }, @@ -207,7 +207,7 @@ # (molecular) electric dipole "dipole": dipoles_auto, # atomic decomposition of the polarizability as ellipsoids. use utility to extract from the ASE frames - "alpha": chemiscope.ase_tensors_to_arrows( + "alpha": chemiscope.ase_tensors_to_ellipsoids( frames, "alpha", force_positive=True, scale=0.2 ), # shapes with a bit of flair diff --git a/src/dataset.ts b/src/dataset.ts index 65505f268..07287b33c 100644 --- a/src/dataset.ts +++ b/src/dataset.ts @@ -3,8 +3,9 @@ * @module main */ -import { CustomShape, Ellipsoid, Sphere } from './structure/shapes'; -import { ShapeData, ShapeParameters } from './structure/shapes'; +import { param } from 'jquery'; +import { CustomShape, Ellipsoid, Sphere, Arrow } from './structure/shapes'; +import { ShapeParameters } from './structure/shapes'; /** A dataset containing all the data to be displayed. */ export interface Dataset { @@ -241,9 +242,21 @@ export function validateDataset(o: JsObject): void { } if ('shapes' in o) { - checkShapes(o.shapes as Record, structureCount, envCount); - - assignShapes(o.shapes as { [name: string]: ShapeParameters }, o.structures as Structure[]); + const check_shape = checkShapes( + o.shapes as Record, + structureCount, + envCount + ); + if (check_shape != '') { + throw 'Error checking shape definitions: ' + check_shape; + } + const check_assign = assignShapes( + o.shapes as { [name: string]: ShapeParameters }, + o.structures as Structure[] + ); + if (check_assign != '') { + throw 'Error assigning shapes to structures: ' + check_assign; + } } if (!('properties' in o)) { @@ -331,51 +344,82 @@ function checkStructures(o: JsObject[]): [number, number] { } function checkShapes( - properties: Record, + shapes: Record, structureCount: number, - envCount: number, - parameters?: Record | undefined -) { - /* - // check to see if all structures have consistent shapes - // placed after structure check to ensure that all structures - // are first validated - if ('shapes' in o[0]) { - const shapeList = Object.keys(o[0].shapes as object); - for (let i = 0; i < o.length; i++) { - const structure = o[i]; - if (!('shapes' in structure)) { - throw Error(`error in structure ${i}: "shape" is not defined`); - } else { - const shapes = structure['shapes'] as Record; - for (const key of shapeList) { - if (!(key in shapes) || shapes[key] === undefined) { - throw Error(`error in structure ${i}: "${key}" is not defined`); - } - } - for (const key of Object.keys(shapes)) { - if (!shapeList.includes(key)) { - throw Error( - `error in structure ${i}: "${key}" is defined, but was not for previous structures` - ); - } - } + envCount: number +): string { + if (typeof shapes !== 'object' || shapes === null) { + return "'shapes' must be an object"; + } + + for (const [key, shape] of Object.entries(shapes as object)) { + if (!('kind' in shape)) { + return `missing "kind" in shape ${key}`; + } + + if (typeof shape.kind !== 'string') { + return `shapes 'kind' must be a string for shape ${key}`; + } + + if ( + shape.kind !== 'sphere' && + shape.kind !== 'ellipsoid' && + shape.kind !== 'arrow' && + shape.kind !== 'custom' + ) { + return `Chemiscope currently only supports custom, ellipsoid, or sphere shapes, got ${shape.kind}`; + } + + if (!('parameters' in shape)) { + return `missing "parameters" in shape ${key}`; + } + + const parameters = shape.parameters as Record; + + if ('structure' in parameters) { + const s_parameters = parameters.structure; + if (!Array.isArray(s_parameters)) { + return `'structure' parameters should be an array in shape ${key}`; + } + + if (s_parameters.length !== structureCount) { + return `'structure' parameters in shape ${key} contain ${s_parameters.length} entries, but there are ${structureCount} structures.`; } } - } else { - for (let i = 0; i < o.length; i++) { - const structure = o[i]; - if ('shapes' in structure) { - throw Error( - `error in structure ${i}: "shape" is defined, but was not for previous structures` - ); + + if ('atom' in parameters) { + const a_parameters = parameters.atom; + if (!Array.isArray(a_parameters)) { + return `'atom' parameters should be an array in shape ${key}`; + } + + if (a_parameters.length !== envCount) { + return `'atom' parameters in shape ${key} contain ${a_parameters.length} entries, but there are ${envCount} environments.`; } } } - */ + + return ''; } -function assignShapes(shapes: { [name: string]: ShapeParameters }, structures: Structure[]) { +function validateShape(kind: string, parameters: Record): string { + if (kind === 'sphere') { + return Sphere.validateParameters(parameters); + } else if (kind === 'ellipsoid') { + return Ellipsoid.validateParameters(parameters); + } else if (kind === 'arrow') { + return Arrow.validateParameters(parameters); + } else if (kind === 'custom') { + return CustomShape.validateParameters(parameters); + } + return ''; +} + +function assignShapes( + shapes: { [name: string]: ShapeParameters }, + structures: Structure[] +): string { + // creates shapes associated with actual structures by combining all the information given in the definition let atomsCount = 0; for (let i_structure = 0; i_structure < structures.length; i_structure++) { const structure = structures[i_structure]; @@ -386,19 +430,40 @@ function assignShapes(shapes: { [name: string]: ShapeParameters }, structures: S structure: shape.parameters.structure, atom: shape.parameters.atom, }; + + let full_parameters = shape.parameters.global; if (parameters.structure) { parameters.structure = [parameters.structure[i_structure]]; + full_parameters = { ...full_parameters, ...parameters.structure[0] }; } + if (parameters.atom) { parameters.atom = parameters.atom.slice(atomsCount, atomsCount + structure.size); + + for (const atom of parameters.atom) { + const atom_parameters = { ...full_parameters, ...atom }; + const check = validateShape(shape.kind, atom_parameters); + if (check !== '') { + return `Validation error for an atom in shape ${name}: ${check}`; + } + } + } else { + const check = validateShape(shape.kind, full_parameters); + if (check !== '') { + return `Validation error for a structure in shape ${name}: ${check}`; + } } + structure.shapes[name] = { kind: shape.kind, parameters: parameters, }; } + atomsCount += structure.size; } + + return ''; // success! } /** @@ -435,60 +500,6 @@ export function checkStructure(s: JsObject): string { } } - if ('shapes' in s) { - const shapes = s.shapes; - - if (typeof shapes !== 'object' || shapes === null) { - return "'shapes' must be an object"; - } - - for (const [key, array] of Object.entries(s.shapes as object)) { - /* - if (!Array.isArray(array)) { - return `shape['${key}'] must be an array`; - } - - if (s.size > 0 && array.length !== s.size) { - return `wrong size for "shape['${key}']", expected ${s.size}, got ${array.length}`; - } - - for (let i = 0; i < array.length; i++) { - const element = array[i] as unknown; - if (typeof element !== 'object' || element === null) { - return "'shapes' entries must be objects"; - } - const shape = element as JsObject; - - if (!('kind' in shape)) { - return `missing "kind" in shape for particle ${i}`; - } - - if (typeof shape.kind !== 'string') { - return `shapes 'kind' must be a string for particle ${i}`; - } - - if (shape.kind === 'sphere') { - const check = Sphere.validateParameters(shape); - if (check !== '') { - return check; - } - } else if (shape.kind === 'ellipsoid') { - const check = Ellipsoid.validateParameters(shape); - if (check !== '') { - return check; - } - } else if (shape.kind === 'custom') { - const check = CustomShape.validateParameters(shape); - if (check !== '') { - return check; - } - } else { - return `Chemiscope currently only supports custom, ellipsoid, or sphere shapes, got ${shape.kind}`; - } - } MCCOMMENT*/ - } - } - return ''; } diff --git a/src/structure/shapes.ts b/src/structure/shapes.ts index c923ea2d4..487d36677 100644 --- a/src/structure/shapes.ts +++ b/src/structure/shapes.ts @@ -274,8 +274,6 @@ export class Sphere extends Shape { } public static validateParameters(parameters: Record): string { - assert(parameters.kind === 'sphere'); - if (!('radius' in parameters)) { return '"radius" is required for "sphere" shapes'; } @@ -333,8 +331,6 @@ export class Ellipsoid extends Shape { } public static validateParameters(parameters: Record): string { - assert(parameters.kind === 'ellipsoid'); - if (!('semiaxes' in parameters)) { return '"semiaxes" is required for "ellipsoid" shapes'; } @@ -439,6 +435,7 @@ function triangulateArrow( const vertices: XYZ[] = []; vertices.push({ x: 0, y: 0, z: 0 }); + // the arrow is built as a surface of revolution, by stacking _|\ motifs for (let i = 0; i < resolution; i++) { // nb replicated points are needed to get sharp edges vertices.push(multXYZ(circle_points[i], base_radius)); @@ -500,8 +497,6 @@ export class Arrow extends Shape { } public static validateParameters(parameters: Record): string { - assert(parameters.kind === 'arrow'); - if (!('vector' in parameters)) { return '"vector" is required for "arrow" shapes'; } @@ -570,8 +565,6 @@ export class CustomShape extends Shape { } public static validateParameters(parameters: Record): string { - assert(parameters.kind === 'custom'); - if (!('vertices' in parameters)) { return '"vertices" is required for "custom" shapes'; }