Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional coefficient scaling and fraction logic #3

Merged
merged 8 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
112 changes: 77 additions & 35 deletions src/models/Chemistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CheckerResponse, ChemicalSymbol, 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';
Expand Down Expand Up @@ -75,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;
Expand Down Expand Up @@ -109,6 +109,9 @@ export interface ChemAST {
result: Result;
}

const STARTING_COEFFICIENT: Fraction = { numerator: 0, denominator: 1 };
const EQUAL_COEFFICIENT: Fraction = { numerator: 1, denominator: 1 };

function augmentNode<T extends ASTNode>(node: T): T {
// The if statements signal to the type checker what we already know
switch (node.type) {
Expand Down Expand Up @@ -243,14 +246,17 @@ export function augment(ast: ChemAST): ChemAST {
return { result: augmentedResult };
}

function checkCoefficient(coeff1: Coefficient, coeff2: Coefficient): boolean {
function checkCoefficient(coeff1: Fraction, coeff2: Fraction): Fraction {
if (coeff1.denominator === 0 || coeff2.denominator === 0) {
console.error("[server] divide by 0 encountered returning false!");
return false;
throw new Error("Division by zero is undefined!");
}

// 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 {
Expand Down Expand Up @@ -288,11 +294,10 @@ 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) {
response.isEqual = response.isEqual &&
test.value === target.value &&
test.coeff === target.coeff;
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
}

if (test.bracketed) {
Expand Down Expand Up @@ -337,15 +342,20 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon
else if (isCompound(test) && isCompound(target)) {
if (test.elements && target.elements) {

if (!response.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 (!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
response.sameElements = false;
response.isEqual = false;
}
else {
response.isEqual = false;
}
}
}

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);
Expand All @@ -360,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;
}
}
Expand All @@ -384,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;
}
}
Expand All @@ -394,9 +404,42 @@ 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;
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) {
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;
}
}
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.
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;
Expand All @@ -411,12 +454,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<ChemicalSymbol, number | undefined>;
newResponse.atomCount[key as ChemicalSymbol] = (value ?? 0) * test.coeff.numerator;
}
newResponse.atomCount = {} as Record<ChemicalSymbol, Fraction | undefined>;
newResponse.atomCount[key as ChemicalSymbol] = MultFrac({numerator: value ?? 0, denominator: 1}, test.coeff)
}
};
newResponse.termAtomCount = {} as Record<ChemicalSymbol, number | undefined>;
}
Expand All @@ -426,8 +469,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;
}
Expand All @@ -436,14 +478,14 @@ 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)) {
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;
Expand All @@ -452,7 +494,8 @@ 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) {
Expand All @@ -467,10 +510,9 @@ 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,
receivedType: test.result.type,
typeMismatch: false,
Expand All @@ -482,9 +524,9 @@ export function check(test: ChemAST, target: ChemAST, allowPermutations?: boolea
isBalanced: true,
isEqual: true,
isNuclear: false,
balanceCount: {} as Record<ChemicalSymbol, number | undefined>,
chargeCount: 0,
allowPermutations: allowPermutations ?? false
options: options,
coefficientScalingValue: STARTING_COEFFICIENT,
}
// Return shortcut response
if (target.result.type === "error" || test.result.type === "error") {
Expand All @@ -494,7 +536,7 @@ export function check(test: ChemAST, target: ChemAST, allowPermutations?: boolea
(isParseError(test.result) ? test.result.value : "No error found");

response.containsError = true;
response.error = { message: message };
response.error = message;
response.isEqual = false;
return response;
}
Expand Down
12 changes: 5 additions & 7 deletions src/models/Nuclear.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -259,10 +259,9 @@ function checkNodesEqual(test: ASTNode, target: ASTNode, response: CheckerRespon
}
}

export function check(test: NuclearAST, target: NuclearAST, allowPermutations?: boolean): CheckerResponse {
const response = {
export function check(test: NuclearAST, target: NuclearAST): CheckerResponse {
const response: CheckerResponse = {
containsError: false,
error: { message: "" },
expectedType: target.result.type,
receivedType: test.result.type,
typeMismatch: false,
Expand All @@ -272,7 +271,6 @@ export function check(test: NuclearAST, target: NuclearAST, allowPermutations?:
isBalanced: true,
isEqual: true,
isNuclear: true,
allowPermutations: allowPermutations ?? false,
}
// Return shortcut response
if (target.result.type === "error" || test.result.type === "error") {
Expand All @@ -282,7 +280,7 @@ export function check(test: NuclearAST, target: NuclearAST, allowPermutations?:
(isParseError(test.result) ? test.result.value : "No error found");

response.containsError = true;
response.error = { message: message };
response.error = message;
response.isEqual = false;
return response;
}
Expand Down
31 changes: 25 additions & 6 deletions src/models/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ 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;
}

export interface ChemistryOptions {
allowPermutations: boolean;
allowScalingCoefficients: boolean;
}

export interface CheckerResponse {
containsError: boolean;
error: { message: string; };
expectedType: ReturnType;
receivedType: ReturnType;
isBalanced: boolean;
Expand All @@ -22,21 +24,23 @@ export interface CheckerResponse {
sameState: boolean;
sameCoefficient: boolean;
sameElements: boolean;
allowPermutations: boolean;
// properties dependent on type
sameArrow?: boolean;
sameBrackets?: boolean;
balancedCharge?: boolean;
validAtomicNumber?: boolean;
balancedAtom?: boolean;
balancedMass?: boolean;
coefficientScalingValue?: Fraction;
error?: string;
// book keeping
checkingPermutations? : boolean;
termAtomCount?: Record<ChemicalSymbol, number | undefined>;
bracketAtomCount?: Record<ChemicalSymbol, number | undefined>;
atomCount?: Record<ChemicalSymbol, number | undefined>;
atomCount?: Record<ChemicalSymbol, Fraction | undefined>;
chargeCount?: number;
nucleonCount?: [number, number];
options?: ChemistryOptions;
}

export function listComparison<T>(
Expand Down Expand Up @@ -99,3 +103,18 @@ export function listComparison<T>(
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});
}
10 changes: 7 additions & 3 deletions src/routes/Chemistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -26,8 +26,12 @@ 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",
}
const result: CheckerResponse = check(test, target, options);

res.status(201).send(result);

Expand Down
Loading