From e65aeaad45d53bec7d191d07021cc0d2a906c6d8 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:51:43 +0100 Subject: [PATCH 1/8] Add options as its own dictionary --- src/models/Chemistry.ts | 19 +++++++++---------- src/models/common.ts | 8 +++++++- src/routes/Chemistry.ts | 11 ++++++++--- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index 4fed4ec..6b16efd 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -1,4 +1,4 @@ -import { CheckerResponse, ChemicalSymbol, Coefficient, listComparison } from './common' +import { CheckerResponse, ChemicalSymbol, ChemistryOptions, Coefficient, listComparison } from './common' import isEqual from "lodash/isEqual"; export type Type = 'error'|'element'|'bracket'|'compound'|'ion'|'term'|'expr'|'statement'|'electron'; @@ -288,7 +288,7 @@ function typesMatch(compound1: (Element | Bracket)[], compound2: (Element | Brac function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerResponse): CheckerResponse { if (isElement(test) && isElement(target)) { - if (!response.allowPermutations || !test.compounded) { + if (!response.options.allowPermutations || !test.compounded) { response.isEqual = response.isEqual && test.value === target.value && test.coeff === target.coeff; @@ -337,7 +337,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isCompound(test) && isCompound(target)) { if (test.elements && target.elements) { - if (!response.allowPermutations) { + if (!response.options.allowPermutations) { if (test.elements.length !== target.elements.length || !typesMatch(test.elements, target.elements) || !isEqual(test, target)) { // TODO: Implement special cases for certain permutations e.g. reverse of an ion chain response.sameElements = false; @@ -345,7 +345,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } } - if (response.allowPermutations && !response.checkingPermutations) { + if (response.options.allowPermutations && !response.checkingPermutations) { const permutationResponse = structuredClone(response); permutationResponse.checkingPermutations = true; const testResponse = listComparison(test.elements, test.elements, permutationResponse, checkNodesEqual); @@ -443,7 +443,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isStatement(test) && isStatement(target)) { const leftResponse = checkNodesEqual(test.left, target.left, response); - const leftBalanceCount = structuredClone(leftResponse.atomCount); + const leftAtomCount = structuredClone(leftResponse.atomCount); const leftChargeCount = structuredClone(leftResponse.chargeCount); leftResponse.atomCount = undefined; leftResponse.chargeCount = 0; @@ -452,7 +452,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon finalResponse.isEqual = finalResponse.isEqual && test.arrow === target.arrow; finalResponse.sameArrow = test.arrow === target.arrow; - finalResponse.isBalanced = isEqual(leftBalanceCount, finalResponse.atomCount) + finalResponse.isBalanced = isEqual(leftAtomCount, finalResponse.atomCount) finalResponse.balancedCharge = leftChargeCount === finalResponse.chargeCount; if (finalResponse.sameElements && !finalResponse.isBalanced && !finalResponse.sameCoefficient) { @@ -467,8 +467,8 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } } -export function check(test: ChemAST, target: ChemAST, allowPermutations?: boolean): CheckerResponse { - const response = { +export function check(test: ChemAST, target: ChemAST, options: ChemistryOptions): CheckerResponse { + const response: CheckerResponse = { containsError: false, error: { message: "" }, expectedType: target.result.type, @@ -482,9 +482,8 @@ export function check(test: ChemAST, target: ChemAST, allowPermutations?: boolea isBalanced: true, isEqual: true, isNuclear: false, - balanceCount: {} as Record, chargeCount: 0, - allowPermutations: allowPermutations ?? false + options: options, } // Return shortcut response if (target.result.type === "error" || test.result.type === "error") { diff --git a/src/models/common.ts b/src/models/common.ts index 9b5d25b..1304af3 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -10,6 +10,12 @@ export interface Coefficient { denominator: number; } +export interface ChemistryOptions { + allowPermutations: boolean; + allowScalingCoefficients: boolean; + allowStateSymbols: boolean; +} + export interface CheckerResponse { containsError: boolean; error: { message: string; }; @@ -22,7 +28,7 @@ export interface CheckerResponse { sameState: boolean; sameCoefficient: boolean; sameElements: boolean; - allowPermutations: boolean; + options: ChemistryOptions; // properties dependent on type sameArrow?: boolean; sameBrackets?: boolean; diff --git a/src/routes/Chemistry.ts b/src/routes/Chemistry.ts index 185d611..e3b8d22 100644 --- a/src/routes/Chemistry.ts +++ b/src/routes/Chemistry.ts @@ -2,7 +2,7 @@ import { Request, Response, Router } from "express"; import { ValidationChain, body, validationResult } from "express-validator"; import { parseChemistryExpression } from "inequality-grammar"; import { check, augment } from "../models/Chemistry"; -import { CheckerResponse } from "../models/common"; +import { CheckerResponse, ChemistryOptions } from "../models/common"; const router = Router(); @@ -26,8 +26,13 @@ router.post('/check', checkValidationRules, (req: Request, res: Response) => { const target: ChemAST = augment(parseChemistryExpression(req.body.target)[0]); const test: ChemAST = augment(parseChemistryExpression(req.body.test)[0]); - const allowPermutations: boolean = req.body.allowPermutations === "true"; - const result: CheckerResponse = check(test, target, allowPermutations); + const options: ChemistryOptions = { + // The API sends attachments as a string hashmap, so we need to convert them to boolean + allowPermutations: req.body.allowPermutations === "true", + allowScalingCoefficients: req.body.allowScalingCoefficients === "true", + allowStateSymbols: req.body.allowStateSymbols === "true", + } + const result: CheckerResponse = check(test, target, options); res.status(201).send(result); From de9ddeb1f46c0ae10e144410314629f019005383 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:33:54 +0100 Subject: [PATCH 2/8] Add coefficient scaling functionality --- src/models/Chemistry.ts | 41 ++++++++++++++++++++++++++++++++--------- src/models/Nuclear.ts | 6 +++--- src/models/common.ts | 3 ++- src/routes/Chemistry.ts | 3 ++- src/routes/Nuclear.ts | 13 +++++++++---- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index 6b16efd..52eb859 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -1,3 +1,4 @@ +import { start } from 'repl'; import { CheckerResponse, ChemicalSymbol, ChemistryOptions, Coefficient, listComparison } from './common' import isEqual from "lodash/isEqual"; @@ -109,6 +110,10 @@ export interface ChemAST { result: Result; } +const STARTING_COEFFICIENT: Coefficient = { numerator: 0, denominator: 0 }; +const ERROR_COEFFICIENT: Coefficient = { numerator: -1, denominator: -1 }; +const EQUAL_COEFFICIENT: Coefficient = { numerator: 1, denominator: 1 }; + function augmentNode(node: T): T { // The if statements signal to the type checker what we already know switch (node.type) { @@ -243,14 +248,17 @@ export function augment(ast: ChemAST): ChemAST { return { result: augmentedResult }; } -function checkCoefficient(coeff1: Coefficient, coeff2: Coefficient): boolean { +function checkCoefficient(coeff1: Coefficient, coeff2: Coefficient): Coefficient { if (coeff1.denominator === 0 || coeff2.denominator === 0) { console.error("[server] divide by 0 encountered returning false!"); - return false; + return ERROR_COEFFICIENT; } + // a/b = c/d <=> ad = bc given b != 0 and d != 0 // Comparing integers is far better than floats - return coeff1.numerator * coeff2.denominator === coeff2.numerator * coeff1.denominator; + const equalCoefficients = coeff1.numerator * coeff2.denominator === coeff2.numerator * coeff1.denominator; + if (equalCoefficients) { return EQUAL_COEFFICIENT; } + else { return { numerator: coeff1.numerator * coeff2.denominator, denominator: coeff2.numerator * coeff1.denominator }; } } function typesMatch(compound1: (Element | Bracket)[], compound2: (Element | Bracket)[]): boolean { @@ -289,10 +297,8 @@ function typesMatch(compound1: (Element | Bracket)[], compound2: (Element | Brac function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerResponse): CheckerResponse { if (isElement(test) && isElement(target)) { if (!response.options.allowPermutations || !test.compounded) { - response.isEqual = response.isEqual && - test.value === target.value && - test.coeff === target.coeff; response.sameCoefficient = response.sameCoefficient && test.coeff === target.coeff; + response.isEqual = response.isEqual && test.value === target.value && response.sameCoefficient } if (test.bracketed) { @@ -394,12 +400,26 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isTerm(test) && isTerm(target)) { const newResponse = checkNodesEqual(test.value, target.value, response); - const coefficientsMatch: boolean = checkCoefficient(test.coeff, target.coeff); - newResponse.sameCoefficient = newResponse.sameCoefficient && coefficientsMatch; - newResponse.isEqual = newResponse.isEqual && coefficientsMatch; + const coefficientScalingValue: Coefficient = checkCoefficient(test.coeff, target.coeff); + if (response.options.allowScalingCoefficients) { + // If first term: set the scaling value, and coefficients are equal. + if (isEqual(newResponse.coefficientScalingValue, STARTING_COEFFICIENT)) { + newResponse.coefficientScalingValue = coefficientScalingValue; + } + // If not first term: coefficients are equal if multiplied by an equivalent scaling value. + else { + const coefficientsMatch = newResponse.coefficientScalingValue ? isEqual(checkCoefficient(newResponse.coefficientScalingValue, coefficientScalingValue), EQUAL_COEFFICIENT) : true; + newResponse.sameCoefficient = newResponse.sameCoefficient && coefficientsMatch; + } + } else { + // If coefficients are not allowed to be scaled, they must be exactly equal. + newResponse.sameCoefficient = newResponse.sameCoefficient && isEqual(test.coeff, target.coeff); + } + newResponse.isEqual = newResponse.isEqual && newResponse.sameCoefficient; if (!test.isElectron && !target.isElectron) { newResponse.sameState = newResponse.sameState && test.state === target.state; + newResponse.sameState = newResponse.sameState && !(newResponse.options.noStateSymbols && test.state !== ""); newResponse.isEqual = newResponse.isEqual && test.state === target.state; } // else the 'isEqual' will already be false from the checkNodesEqual above @@ -455,6 +475,8 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon finalResponse.isBalanced = isEqual(leftAtomCount, finalResponse.atomCount) finalResponse.balancedCharge = leftChargeCount === finalResponse.chargeCount; + console.log(leftAtomCount, finalResponse.atomCount); + if (finalResponse.sameElements && !finalResponse.isBalanced && !finalResponse.sameCoefficient) { finalResponse.isBalanced = true; } @@ -484,6 +506,7 @@ export function check(test: ChemAST, target: ChemAST, options: ChemistryOptions) isNuclear: false, chargeCount: 0, options: options, + coefficientScalingValue: STARTING_COEFFICIENT, } // Return shortcut response if (target.result.type === "error" || test.result.type === "error") { diff --git a/src/models/Nuclear.ts b/src/models/Nuclear.ts index 15731c2..8d198ef 100644 --- a/src/models/Nuclear.ts +++ b/src/models/Nuclear.ts @@ -1,4 +1,4 @@ -import { CheckerResponse, ChemicalSymbol, chemicalSymbol, listComparison } from './common' +import { CheckerResponse, ChemicalSymbol, chemicalSymbol, ChemistryOptions, listComparison } from './common' export type ParticleString = 'alphaparticle'|'betaparticle'|'gammaray'|'neutrino'|'antineutrino'|'electron'|'positron'|'neutron'|'proton'; export type Type = 'error'|'particle'|'isotope'|'term'|'expr'|'statement'; @@ -259,7 +259,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } } -export function check(test: NuclearAST, target: NuclearAST, allowPermutations?: boolean): CheckerResponse { +export function check(test: NuclearAST, target: NuclearAST, options: ChemistryOptions): CheckerResponse { const response = { containsError: false, error: { message: "" }, @@ -272,7 +272,7 @@ export function check(test: NuclearAST, target: NuclearAST, allowPermutations?: isBalanced: true, isEqual: true, isNuclear: true, - allowPermutations: allowPermutations ?? false, + options } // Return shortcut response if (target.result.type === "error" || test.result.type === "error") { diff --git a/src/models/common.ts b/src/models/common.ts index 1304af3..5699108 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -13,7 +13,7 @@ export interface Coefficient { export interface ChemistryOptions { allowPermutations: boolean; allowScalingCoefficients: boolean; - allowStateSymbols: boolean; + noStateSymbols: boolean; } export interface CheckerResponse { @@ -36,6 +36,7 @@ export interface CheckerResponse { validAtomicNumber?: boolean; balancedAtom?: boolean; balancedMass?: boolean; + coefficientScalingValue?: Coefficient; // book keeping checkingPermutations? : boolean; termAtomCount?: Record; diff --git a/src/routes/Chemistry.ts b/src/routes/Chemistry.ts index e3b8d22..9f2f75f 100644 --- a/src/routes/Chemistry.ts +++ b/src/routes/Chemistry.ts @@ -30,8 +30,9 @@ router.post('/check', checkValidationRules, (req: Request, res: Response) => { // The API sends attachments as a string hashmap, so we need to convert them to boolean allowPermutations: req.body.allowPermutations === "true", allowScalingCoefficients: req.body.allowScalingCoefficients === "true", - allowStateSymbols: req.body.allowStateSymbols === "true", + noStateSymbols: req.body.noStateSymbols === "true", } + console.log("options", options); const result: CheckerResponse = check(test, target, options); res.status(201).send(result); diff --git a/src/routes/Nuclear.ts b/src/routes/Nuclear.ts index 253a5d3..de1504b 100644 --- a/src/routes/Nuclear.ts +++ b/src/routes/Nuclear.ts @@ -2,7 +2,7 @@ import { Request, Response, Router } from "express"; import { ValidationChain, body, validationResult } from "express-validator"; import { parseNuclearExpression } from "inequality-grammar"; import { check, augment } from "../models/Nuclear"; -import { CheckerResponse } from "../models/common"; +import { CheckerResponse, ChemistryOptions } from "../models/common"; const router = Router(); @@ -13,7 +13,7 @@ const checkValidationRules: ValidationChain[] = [ ]; const parseValidationRules: ValidationChain[] = [ body('test').notEmpty().withMessage("mhChem expression is required."), - body('description').optional().isString().withMessage("When provided the descrition must be a string.") + body('description').optional().isString().withMessage("When provided the description must be a string.") ]; router.post('/check', checkValidationRules, (req: Request, res: Response) => { @@ -25,8 +25,13 @@ router.post('/check', checkValidationRules, (req: Request, res: Response) => { const target: NuclearAST = augment(parseNuclearExpression(req.body.target)[0]); const test: NuclearAST = augment(parseNuclearExpression(req.body.test)[0]); - const allowPermutations: boolean = req.body.allowPermutations === "true"; - const result: CheckerResponse = check(test, target, allowPermutations); + const options: ChemistryOptions = { + // The API sends attachments as a string hashmap, so we need to convert them to boolean + allowPermutations: req.body.allowPermutations === "true", + allowScalingCoefficients: req.body.allowScalingCoefficients === "true", + noStateSymbols: req.body.noStateSymbols === "true", + } + const result: CheckerResponse = check(test, target, options); res.status(201).send(result); From 18145492e9346d86e27135f54fa537b8df1d0de8 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:25:07 +0100 Subject: [PATCH 3/8] Add feedback for term count failing early --- src/models/Chemistry.ts | 3 +-- src/routes/Chemistry.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index 52eb859..ecf45b1 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -446,8 +446,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isExpression(test) && isExpression(target)) { if (test.terms && target.terms) { if (test.terms.length !== target.terms.length) { - // TODO: add a new property stating the number of terms was wrong - // fail early if term lengths not the same + response.sameElements = false; response.isEqual = false; return response; } diff --git a/src/routes/Chemistry.ts b/src/routes/Chemistry.ts index 9f2f75f..8d4c232 100644 --- a/src/routes/Chemistry.ts +++ b/src/routes/Chemistry.ts @@ -32,7 +32,6 @@ router.post('/check', checkValidationRules, (req: Request, res: Response) => { allowScalingCoefficients: req.body.allowScalingCoefficients === "true", noStateSymbols: req.body.noStateSymbols === "true", } - console.log("options", options); const result: CheckerResponse = check(test, target, options); res.status(201).send(result); From 624a32b51ebbd7f984d6dd844d45e6c3987c7a65 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:01:51 +0100 Subject: [PATCH 4/8] Apply fraction logic to atom counting --- src/models/Chemistry.ts | 46 ++++++++++++++++++++++------------------- src/models/common.ts | 23 ++++++++++++++++----- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index ecf45b1..28c8af0 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -1,5 +1,4 @@ -import { start } from 'repl'; -import { CheckerResponse, ChemicalSymbol, ChemistryOptions, Coefficient, listComparison } from './common' +import { AddFrac, CheckerResponse, ChemicalSymbol, ChemistryOptions, Fraction, listComparison, MultFrac } from './common' import isEqual from "lodash/isEqual"; export type Type = 'error'|'element'|'bracket'|'compound'|'ion'|'term'|'expr'|'statement'|'electron'; @@ -76,7 +75,7 @@ export function isElectron(node: ASTNode): node is Electron { export interface Term extends ASTNode { type: 'term'; value: Ion | Compound | Electron; - coeff: Coefficient; + coeff: Fraction; state: State; hydrate: number; isElectron: boolean; @@ -110,9 +109,9 @@ export interface ChemAST { result: Result; } -const STARTING_COEFFICIENT: Coefficient = { numerator: 0, denominator: 0 }; -const ERROR_COEFFICIENT: Coefficient = { numerator: -1, denominator: -1 }; -const EQUAL_COEFFICIENT: Coefficient = { numerator: 1, denominator: 1 }; +const STARTING_COEFFICIENT: Fraction = { numerator: 0, denominator: 1 }; +const ERROR_COEFFICIENT: Fraction = { numerator: -1, denominator: -1 }; +const EQUAL_COEFFICIENT: Fraction = { numerator: 1, denominator: 1 }; function augmentNode(node: T): T { // The if statements signal to the type checker what we already know @@ -248,7 +247,7 @@ export function augment(ast: ChemAST): ChemAST { return { result: augmentedResult }; } -function checkCoefficient(coeff1: Coefficient, coeff2: Coefficient): Coefficient { +function checkCoefficient(coeff1: Fraction, coeff2: Fraction): Fraction { if (coeff1.denominator === 0 || coeff2.denominator === 0) { console.error("[server] divide by 0 encountered returning false!"); return ERROR_COEFFICIENT; @@ -298,7 +297,8 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon if (isElement(test) && isElement(target)) { if (!response.options.allowPermutations || !test.compounded) { response.sameCoefficient = response.sameCoefficient && test.coeff === target.coeff; - response.isEqual = response.isEqual && test.value === target.value && response.sameCoefficient + response.sameElements = response.sameElements && test.value === target.value; + response.isEqual = response.isEqual && response.sameElements && response.sameCoefficient } if (test.bracketed) { @@ -344,10 +344,15 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon if (test.elements && target.elements) { if (!response.options.allowPermutations) { - if (test.elements.length !== target.elements.length || !typesMatch(test.elements, target.elements) || !isEqual(test, target)) { - // TODO: Implement special cases for certain permutations e.g. reverse of an ion chain - response.sameElements = false; - response.isEqual = false; + if (!isEqual(test, target)) { + if (test.elements.length !== target.elements.length || !typesMatch(test.elements, target.elements)) { + // TODO: Implement special cases for certain permutations e.g. reverse of an ion chain + response.sameElements = false; + response.isEqual = false; + } + else { + response.isEqual = false; + } } } @@ -400,13 +405,13 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isTerm(test) && isTerm(target)) { const newResponse = checkNodesEqual(test.value, target.value, response); - const coefficientScalingValue: Coefficient = checkCoefficient(test.coeff, target.coeff); + const coefficientScalingValue: Fraction = checkCoefficient(test.coeff, target.coeff); if (response.options.allowScalingCoefficients) { // If first term: set the scaling value, and coefficients are equal. if (isEqual(newResponse.coefficientScalingValue, STARTING_COEFFICIENT)) { newResponse.coefficientScalingValue = coefficientScalingValue; } - // If not first term: coefficients are equal if multiplied by an equivalent scaling value. + // If not first term: coefficients are equal only if multiplied by an equivalent scaling value. else { const coefficientsMatch = newResponse.coefficientScalingValue ? isEqual(checkCoefficient(newResponse.coefficientScalingValue, coefficientScalingValue), EQUAL_COEFFICIENT) : true; newResponse.sameCoefficient = newResponse.sameCoefficient && coefficientsMatch; @@ -431,12 +436,12 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon if (newResponse.termAtomCount) { for (const [key, value] of Object.entries(newResponse.termAtomCount)) { if (newResponse.atomCount) { - newResponse.atomCount[key as ChemicalSymbol] = (newResponse.atomCount[key as ChemicalSymbol] ?? 0) + (value ?? 0) * test.coeff.numerator; + newResponse.atomCount[key as ChemicalSymbol] = AddFrac((newResponse.atomCount[key as ChemicalSymbol] ?? STARTING_COEFFICIENT), MultFrac({numerator: value ?? 0, denominator: 1}, test.coeff)); } else { - newResponse.atomCount = {} as Record; - newResponse.atomCount[key as ChemicalSymbol] = (value ?? 0) * test.coeff.numerator; - } + newResponse.atomCount = {} as Record; + newResponse.atomCount[key as ChemicalSymbol] = MultFrac({numerator: value ?? 0, denominator: 1}, test.coeff) + } }; newResponse.termAtomCount = {} as Record; } @@ -471,11 +476,10 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon finalResponse.isEqual = finalResponse.isEqual && test.arrow === target.arrow; finalResponse.sameArrow = test.arrow === target.arrow; - finalResponse.isBalanced = isEqual(leftAtomCount, finalResponse.atomCount) + finalResponse.isBalanced = isEqual(leftAtomCount, finalResponse.atomCount); + finalResponse.balancedCharge = leftChargeCount === finalResponse.chargeCount; - console.log(leftAtomCount, finalResponse.atomCount); - if (finalResponse.sameElements && !finalResponse.isBalanced && !finalResponse.sameCoefficient) { finalResponse.isBalanced = true; } diff --git a/src/models/common.ts b/src/models/common.ts index 5699108..9105c28 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -3,9 +3,7 @@ export type ChemicalSymbol = typeof chemicalSymbol[number]; export type ReturnType = 'term'|'expr'|'statement'|'error'|'unknown'; -type Aggregates = 'atomCount' | 'chargeCount' | 'nucleonCount'; - -export interface Coefficient { +export interface Fraction { numerator: number; denominator: number; } @@ -36,12 +34,12 @@ export interface CheckerResponse { validAtomicNumber?: boolean; balancedAtom?: boolean; balancedMass?: boolean; - coefficientScalingValue?: Coefficient; + coefficientScalingValue?: Fraction; // book keeping checkingPermutations? : boolean; termAtomCount?: Record; bracketAtomCount?: Record; - atomCount?: Record; + atomCount?: Record; chargeCount?: number; nucleonCount?: [number, number]; } @@ -106,3 +104,18 @@ export function listComparison( return possibleResponse } +const SimplifyFrac = (frac: Fraction): Fraction => { + let gcd = function gcd(a: number, b:number): number{ + return b ? gcd(b, a%b) : a; + }; + const divisor = gcd(frac.numerator, frac.denominator); + return {numerator: frac.numerator / divisor, denominator: frac.denominator / divisor}; +} + +export const AddFrac = (frac1: Fraction, frac2: Fraction): Fraction => { + return SimplifyFrac({numerator: frac1.numerator * frac2.denominator + frac2.numerator * frac1.denominator, denominator: frac1.denominator * frac2.denominator}); +} + +export const MultFrac = (frac1: Fraction, frac2: Fraction): Fraction => { + return SimplifyFrac({numerator: frac1.numerator * frac2.numerator, denominator: frac1.denominator * frac2.denominator}); +} From ce10a0d97752ca5c759b41a75bc3bd6b9e4cc28f Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:51:50 +0100 Subject: [PATCH 5/8] Remove options as unnecessary from Nuclear Qs --- src/models/Chemistry.ts | 9 ++++----- src/models/Nuclear.ts | 3 +-- src/models/common.ts | 3 +-- src/routes/Chemistry.ts | 1 - src/routes/Nuclear.ts | 8 +------- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index 28c8af0..6a4aa60 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -295,7 +295,7 @@ function typesMatch(compound1: (Element | Bracket)[], compound2: (Element | Brac function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerResponse): CheckerResponse { if (isElement(test) && isElement(target)) { - if (!response.options.allowPermutations || !test.compounded) { + if (!response.options?.allowPermutations || !test.compounded) { response.sameCoefficient = response.sameCoefficient && test.coeff === target.coeff; response.sameElements = response.sameElements && test.value === target.value; response.isEqual = response.isEqual && response.sameElements && response.sameCoefficient @@ -343,7 +343,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isCompound(test) && isCompound(target)) { if (test.elements && target.elements) { - if (!response.options.allowPermutations) { + if (!response.options?.allowPermutations) { if (!isEqual(test, target)) { if (test.elements.length !== target.elements.length || !typesMatch(test.elements, target.elements)) { // TODO: Implement special cases for certain permutations e.g. reverse of an ion chain @@ -356,7 +356,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } } - if (response.options.allowPermutations && !response.checkingPermutations) { + if (response.options?.allowPermutations && !response.checkingPermutations) { const permutationResponse = structuredClone(response); permutationResponse.checkingPermutations = true; const testResponse = listComparison(test.elements, test.elements, permutationResponse, checkNodesEqual); @@ -406,7 +406,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon const newResponse = checkNodesEqual(test.value, target.value, response); const coefficientScalingValue: Fraction = checkCoefficient(test.coeff, target.coeff); - if (response.options.allowScalingCoefficients) { + if (response.options?.allowScalingCoefficients) { // If first term: set the scaling value, and coefficients are equal. if (isEqual(newResponse.coefficientScalingValue, STARTING_COEFFICIENT)) { newResponse.coefficientScalingValue = coefficientScalingValue; @@ -424,7 +424,6 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon if (!test.isElectron && !target.isElectron) { newResponse.sameState = newResponse.sameState && test.state === target.state; - newResponse.sameState = newResponse.sameState && !(newResponse.options.noStateSymbols && test.state !== ""); newResponse.isEqual = newResponse.isEqual && test.state === target.state; } // else the 'isEqual' will already be false from the checkNodesEqual above diff --git a/src/models/Nuclear.ts b/src/models/Nuclear.ts index 8d198ef..c4af8e9 100644 --- a/src/models/Nuclear.ts +++ b/src/models/Nuclear.ts @@ -259,7 +259,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } } -export function check(test: NuclearAST, target: NuclearAST, options: ChemistryOptions): CheckerResponse { +export function check(test: NuclearAST, target: NuclearAST): CheckerResponse { const response = { containsError: false, error: { message: "" }, @@ -272,7 +272,6 @@ export function check(test: NuclearAST, target: NuclearAST, options: ChemistryOp isBalanced: true, isEqual: true, isNuclear: true, - options } // Return shortcut response if (target.result.type === "error" || test.result.type === "error") { diff --git a/src/models/common.ts b/src/models/common.ts index 9105c28..e9d0215 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -11,7 +11,6 @@ export interface Fraction { export interface ChemistryOptions { allowPermutations: boolean; allowScalingCoefficients: boolean; - noStateSymbols: boolean; } export interface CheckerResponse { @@ -26,7 +25,6 @@ export interface CheckerResponse { sameState: boolean; sameCoefficient: boolean; sameElements: boolean; - options: ChemistryOptions; // properties dependent on type sameArrow?: boolean; sameBrackets?: boolean; @@ -42,6 +40,7 @@ export interface CheckerResponse { atomCount?: Record; chargeCount?: number; nucleonCount?: [number, number]; + options?: ChemistryOptions; } export function listComparison( diff --git a/src/routes/Chemistry.ts b/src/routes/Chemistry.ts index 8d4c232..168be0a 100644 --- a/src/routes/Chemistry.ts +++ b/src/routes/Chemistry.ts @@ -30,7 +30,6 @@ router.post('/check', checkValidationRules, (req: Request, res: Response) => { // The API sends attachments as a string hashmap, so we need to convert them to boolean allowPermutations: req.body.allowPermutations === "true", allowScalingCoefficients: req.body.allowScalingCoefficients === "true", - noStateSymbols: req.body.noStateSymbols === "true", } const result: CheckerResponse = check(test, target, options); diff --git a/src/routes/Nuclear.ts b/src/routes/Nuclear.ts index de1504b..c3f0397 100644 --- a/src/routes/Nuclear.ts +++ b/src/routes/Nuclear.ts @@ -25,13 +25,7 @@ router.post('/check', checkValidationRules, (req: Request, res: Response) => { const target: NuclearAST = augment(parseNuclearExpression(req.body.target)[0]); const test: NuclearAST = augment(parseNuclearExpression(req.body.test)[0]); - const options: ChemistryOptions = { - // The API sends attachments as a string hashmap, so we need to convert them to boolean - allowPermutations: req.body.allowPermutations === "true", - allowScalingCoefficients: req.body.allowScalingCoefficients === "true", - noStateSymbols: req.body.noStateSymbols === "true", - } - const result: CheckerResponse = check(test, target, options); + const result: CheckerResponse = check(test, target); res.status(201).send(result); From 01bf3a3c3117d4d377457882dbea5a95a5ee937a Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:41:05 +0100 Subject: [PATCH 6/8] Change ERROR_COEFFICIENT to thrown error --- src/models/Chemistry.ts | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index 6a4aa60..7ca4166 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -110,7 +110,6 @@ export interface ChemAST { } const STARTING_COEFFICIENT: Fraction = { numerator: 0, denominator: 1 }; -const ERROR_COEFFICIENT: Fraction = { numerator: -1, denominator: -1 }; const EQUAL_COEFFICIENT: Fraction = { numerator: 1, denominator: 1 }; function augmentNode(node: T): T { @@ -250,7 +249,7 @@ export function augment(ast: ChemAST): ChemAST { function checkCoefficient(coeff1: Fraction, coeff2: Fraction): Fraction { if (coeff1.denominator === 0 || coeff2.denominator === 0) { console.error("[server] divide by 0 encountered returning false!"); - return ERROR_COEFFICIENT; + throw new Error("Division by zero is undefined!"); } // a/b = c/d <=> ad = bc given b != 0 and d != 0 @@ -405,16 +404,36 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon else if (isTerm(test) && isTerm(target)) { const newResponse = checkNodesEqual(test.value, target.value, response); + try { const coefficientScalingValue: Fraction = checkCoefficient(test.coeff, target.coeff); + } + catch (e) { + response.containsError = true; + response.error = (e as Error).message; + response.isEqual = false; + return response; + } + const coefficientScalingValue: Fraction = STARTING_COEFFICIENT if (response.options?.allowScalingCoefficients) { - // If first term: set the scaling value, and coefficients are equal. - if (isEqual(newResponse.coefficientScalingValue, STARTING_COEFFICIENT)) { - newResponse.coefficientScalingValue = coefficientScalingValue; + try { + + + // If first term: set the scaling value, and coefficients are equal. + if (isEqual(newResponse.coefficientScalingValue, STARTING_COEFFICIENT)) { + newResponse.coefficientScalingValue = coefficientScalingValue; + } + // If not first term: coefficients are equal only if multiplied by an equivalent scaling value. + else { + const scalingValueRatio: Fraction = newResponse.coefficientScalingValue ? checkCoefficient(newResponse.coefficientScalingValue, coefficientScalingValue) : EQUAL_COEFFICIENT; + const coefficientsMatch = isEqual(scalingValueRatio, EQUAL_COEFFICIENT) + newResponse.sameCoefficient = newResponse.sameCoefficient && coefficientsMatch; + } } - // If not first term: coefficients are equal only if multiplied by an equivalent scaling value. - else { - const coefficientsMatch = newResponse.coefficientScalingValue ? isEqual(checkCoefficient(newResponse.coefficientScalingValue, coefficientScalingValue), EQUAL_COEFFICIENT) : true; - newResponse.sameCoefficient = newResponse.sameCoefficient && coefficientsMatch; + catch (e) { + response.containsError = true; + response.error = (e as Error).message; + response.isEqual = false; + return response; } } else { // If coefficients are not allowed to be scaled, they must be exactly equal. From 834cb48628673da5742c38ce453e531a50532b62 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:42:07 +0100 Subject: [PATCH 7/8] Update to new error formatting Developed in parallel in hotfix/feedback-fixes --- src/models/Chemistry.ts | 9 ++++----- src/models/Nuclear.ts | 7 +++---- src/models/common.ts | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/models/Chemistry.ts b/src/models/Chemistry.ts index 7ca4166..f3fb2e3 100644 --- a/src/models/Chemistry.ts +++ b/src/models/Chemistry.ts @@ -370,7 +370,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } else { console.error("[server] Encountered unaugmented AST. Returning error"); response.containsError = true; - response.error = { message: "Received unaugmented AST during checking process." }; + response.error = "Received unaugmented AST during checking process."; return response; } } @@ -394,7 +394,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } else { console.error("[server] Encountered unaugmented AST. Returning error"); response.containsError = true; - response.error = { message: "Received unaugmented AST during checking process." }; + response.error = "Received unaugmented AST during checking process."; return response; } } @@ -478,7 +478,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } else { console.error("[server] Encountered unaugmented AST. Returning error"); response.containsError = true; - response.error = { message: "Received unaugmented AST during checking process." }; + response.error = "Received unaugmented AST during checking process."; return response; } } @@ -513,7 +513,6 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon export function check(test: ChemAST, target: ChemAST, options: ChemistryOptions): CheckerResponse { const response: CheckerResponse = { containsError: false, - error: { message: "" }, expectedType: target.result.type, receivedType: test.result.type, typeMismatch: false, @@ -537,7 +536,7 @@ export function check(test: ChemAST, target: ChemAST, options: ChemistryOptions) (isParseError(test.result) ? test.result.value : "No error found"); response.containsError = true; - response.error = { message: message }; + response.error = message; response.isEqual = false; return response; } diff --git a/src/models/Nuclear.ts b/src/models/Nuclear.ts index c4af8e9..f2101ec 100644 --- a/src/models/Nuclear.ts +++ b/src/models/Nuclear.ts @@ -231,7 +231,7 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } else { console.error("[server] Encountered unaugmented AST. Returning error"); response.containsError = true; - response.error = { message: "Received unaugmented AST during checking process." }; + response.error = "Received unaugmented AST during checking process."; return response; } } else if (isStatement(test) && isStatement(target)) { @@ -260,9 +260,8 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon } export function check(test: NuclearAST, target: NuclearAST): CheckerResponse { - const response = { + const response: CheckerResponse = { containsError: false, - error: { message: "" }, expectedType: target.result.type, receivedType: test.result.type, typeMismatch: false, @@ -281,7 +280,7 @@ export function check(test: NuclearAST, target: NuclearAST): CheckerResponse { (isParseError(test.result) ? test.result.value : "No error found"); response.containsError = true; - response.error = { message: message }; + response.error = message; response.isEqual = false; return response; } diff --git a/src/models/common.ts b/src/models/common.ts index e9d0215..8d7554d 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -15,7 +15,6 @@ export interface ChemistryOptions { export interface CheckerResponse { containsError: boolean; - error: { message: string; }; expectedType: ReturnType; receivedType: ReturnType; isBalanced: boolean; @@ -33,6 +32,7 @@ export interface CheckerResponse { balancedAtom?: boolean; balancedMass?: boolean; coefficientScalingValue?: Fraction; + error?: string; // book keeping checkingPermutations? : boolean; termAtomCount?: Record; From c7b655c9ab2af6b66372f84652ab8b844f02a0d7 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:52:44 +0100 Subject: [PATCH 8/8] Update Inequality Grammar to 1.3.4 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 04e93c2..f5d7d5c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-validator": "^7.1.0", - "inequality-grammar": "^1.3.2", + "inequality-grammar": "^1.3.4", "lodash": "^4.17.21" } } diff --git a/yarn.lock b/yarn.lock index 4772bfe..36ef3cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1617,10 +1617,10 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -inequality-grammar@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/inequality-grammar/-/inequality-grammar-1.3.2.tgz#119a8afb3ea802c8abfe44bf561fde38a24a816e" - integrity sha512-mlwS1kXr3z60NcSXCv1CFXf7VSHXPgZzjSzN4hQSaSHJSkFGb4cJJONT3M+PiLUgUwlsjOEUBFLxE8p9V6nlWQ== +inequality-grammar@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/inequality-grammar/-/inequality-grammar-1.3.4.tgz#22b34c7fbb6c61e5567f3c4a290b212500ea5881" + integrity sha512-LyRxj57cC8xW+OAj15WAK7hA3BblZtNLl8fDyXP9x/mn5hmd44bCCkWRsBdDqXZZZAwdmLCjPfAPJh48/sJaMw== dependencies: lodash "^4.17.21" moo "^0.5.2"