diff --git a/playwright.config.mjs b/playwright.config.mjs index 2c18b63f..bc920a2e 100644 --- a/playwright.config.mjs +++ b/playwright.config.mjs @@ -27,7 +27,7 @@ const config = { forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 4 : 8, + workers: process.env.CI ? 4 : 4, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'github' : 'list', reportSlowTests: null, diff --git a/public/dimensional_analysis.py b/public/dimensional_analysis.py index b5f202b5..d7dfed95 100644 --- a/public/dimensional_analysis.py +++ b/public/dimensional_analysis.py @@ -12,6 +12,8 @@ import traceback from importlib import import_module +import collections + from json import loads, dumps import math @@ -97,12 +99,14 @@ class ExprWithAssumptions(Expr): amount_of_substance, angle, information) dimension_symbols = set((dimension.name for dimension in dimensions)) -from sympy.physics.units.systems.si import dimsys_SI +from sympy.physics.units.systems.si import dimsys_SI, DimensionSystem from sympy.utilities.iterables import topological_sort from sympy.utilities.lambdify import lambdify, implemented_function +from sympy.functions.elementary.trigonometric import TrigonometricFunction + import numbers from typing import TypedDict, Literal, cast, TypeGuard, Sequence, Any, Callable, NotRequired @@ -744,6 +748,86 @@ def get_base_units(custom_base_units: CustomBaseUnits | None= None) -> dict[tupl ZERO_PLACEHOLDER = "implicit_param__zero" +# Monkey patch of SymPy's get_dimensional_dependencies so that units that have a small +# exponent difference (within EXP_NUM_DIGITS) are still considered equivalent for addition +def custom_get_dimensional_dependencies_for_name(self, dimension): + if isinstance(dimension, str): + dimension = Dimension(Symbol(dimension)) + elif not isinstance(dimension, Dimension): + dimension = Dimension(dimension) + + if dimension.name.is_Symbol: + # Dimensions not included in the dependencies are considered + # as base dimensions: + return dict(self.dimensional_dependencies.get(dimension, {dimension: 1})) + + if dimension.name.is_number or dimension.name.is_NumberSymbol: + return {} + + get_for_name = self._get_dimensional_dependencies_for_name + + if dimension.name.is_Mul: + ret = collections.defaultdict(int) + dicts = [get_for_name(i) for i in dimension.name.args] + for d in dicts: + for k, v in d.items(): + ret[k] += v + return {k: v for (k, v) in ret.items() if v != 0} + + if dimension.name.is_Add: + dicts = [get_for_name(i) for i in dimension.name.args] + + for d in dicts: + keys_to_remove = set() + for key, exp in d.items(): + new_exp = exp.round(EXP_NUM_DIGITS) + if new_exp == sympify("0"): + keys_to_remove.add(key) + else: + d[key] = new_exp + + for key in keys_to_remove: + d.pop(key) + + if all(d == dicts[0] for d in dicts[1:]): + return dicts[0] + raise TypeError("Only equivalent dimensions can be added or subtracted.") + + if dimension.name.is_Pow: + dim_base = get_for_name(dimension.name.base) + dim_exp = get_for_name(dimension.name.exp) + if dim_exp == {} or dimension.name.exp.is_Symbol: + return {k: v * dimension.name.exp for (k, v) in dim_base.items()} + else: + raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.") + + if dimension.name.is_Function: + args = (Dimension._from_dimensional_dependencies( # type: ignore + get_for_name(arg)) for arg in dimension.name.args) + result = dimension.name.func(*args) + + dicts = [get_for_name(i) for i in dimension.name.args] + + if isinstance(result, Dimension): + return self.get_dimensional_dependencies(result) + elif result.func == dimension.name.func: + if isinstance(dimension.name, TrigonometricFunction): + if dicts[0] in ({}, {Dimension('angle'): 1}): + return {} + else: + raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(dimension.func)) + else: + if all(item == {} for item in dicts): + return {} + else: + raise TypeError("The input arguments for the function {} must be dimensionless.".format(dimension.func)) + else: + return get_for_name(result) + + raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(dimension.name))) + +DimensionSystem._get_dimensional_dependencies_for_name = custom_get_dimensional_dependencies_for_name # type: ignore + def round_exp(value: float) -> float | int: value = round(value, EXP_NUM_DIGITS) @@ -1177,7 +1261,7 @@ def custom_pow(base: Expr, exponent: Expr): def custom_pow_dims(dim_values: DimValues, base: Expr, exponent: Expr): if custom_get_dimensional_dependencies(exponent) != {}: raise TypeError('Exponent Not Dimensionless') - return Pow(base, dim_values["args"][1]) + return Pow(base.evalf(PRECISION), (dim_values["args"][1]).evalf(PRECISION)) CP = None diff --git a/tests/test_basic.spec.mjs b/tests/test_basic.spec.mjs index 95ea83d8..2793042d 100644 --- a/tests/test_basic.spec.mjs +++ b/tests/test_basic.spec.mjs @@ -780,6 +780,27 @@ test('Test zero canceling bug with exponent', async () => { expect(content).toBe('m'); }); +test('Test floating point exponent rounding', async () => { + await page.setLatex(0, String.raw`1\left\lbrack m\right\rbrack+1\left\lbrack\frac{N^{\frac13}}{m^{\frac23}}\right\rbrack\cdot1\left\lbrack\frac{m^{\frac53}}{N^{\frac13}}\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(1, String.raw`1\left\lbrack kg\cdot s^{.0000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); + await page.click('#add-math-cell'); + await page.setLatex(2, String.raw`1\left\lbrack kg\cdot s^{.000000000001}\right\rbrack+2\left\lbrack kg\right\rbrack=`); + + await page.waitForSelector('text=Updating...', {state: 'detached'}); + + let content = await page.textContent('#result-value-0'); + expect(parseLatexFloat(content)).toBeCloseTo(2, precision); + content = await page.textContent('#result-units-0'); + expect(content).toBe('m'); + + content = await page.textContent('#result-value-1'); + expect(parseLatexFloat(content)).toBeCloseTo(3, precision); + content = await page.textContent('#result-units-1'); + expect(content).toBe('kg'); + + await expect(page.locator('#cell-2 >> text=Dimension Error')).toBeVisible(); +}); test('Test function notation with integrals', async () => { @@ -804,7 +825,6 @@ test('Test function notation with integrals', async () => { }); - test('Test greek characters as variables', async () => { await page.type(':nth-match(math-field.editable, 1)', 'alpha+beta+gamma+delta+epsilon+zeta+eta+theta+iota+kappa+lambda+' +