@classytic/stage 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +5 -1
- package/dist/_virtual/_rolldown/runtime.mjs +18 -0
- package/dist/assets/index.mjs +0 -1
- package/dist/atomic/aufbau.d.mts +144 -0
- package/dist/atomic/aufbau.mjs +384 -0
- package/dist/atomic/elements.d.mts +67 -0
- package/dist/atomic/elements.mjs +785 -0
- package/dist/atomic/emission.d.mts +112 -0
- package/dist/atomic/emission.mjs +179 -0
- package/dist/atomic/index.d.mts +8 -0
- package/dist/atomic/index.mjs +9 -0
- package/dist/atomic/periodic.d.mts +82 -0
- package/dist/atomic/periodic.mjs +231 -0
- package/dist/atomic/properties.d.mts +100 -0
- package/dist/atomic/properties.mjs +574 -0
- package/dist/atomic/shells.d.mts +137 -0
- package/dist/atomic/shells.mjs +170 -0
- package/dist/atomic/trends.d.mts +148 -0
- package/dist/atomic/trends.mjs +225 -0
- package/dist/builder/Palette.mjs +50 -89
- package/dist/builder/SceneBuilder.mjs +15 -73
- package/dist/chem/chromatography.d.mts +66 -0
- package/dist/chem/chromatography.mjs +63 -0
- package/dist/chem/complex.d.mts +114 -0
- package/dist/chem/complex.mjs +234 -0
- package/dist/chem/cycle.d.mts +75 -0
- package/dist/chem/cycle.mjs +145 -0
- package/dist/chem/equation.d.mts +86 -0
- package/dist/chem/equation.mjs +168 -0
- package/dist/chem/formula.d.mts +90 -0
- package/dist/chem/formula.mjs +201 -0
- package/dist/chem/functional-groups.d.mts +31 -0
- package/dist/chem/functional-groups.mjs +187 -0
- package/dist/chem/hazard.d.mts +94 -0
- package/dist/chem/hazard.mjs +329 -0
- package/dist/chem/index.d.mts +34 -33
- package/dist/chem/index.mjs +34 -28
- package/dist/chem/ionic.d.mts +98 -0
- package/dist/chem/ionic.mjs +156 -0
- package/dist/chem/layout.d.mts +46 -0
- package/dist/chem/layout.mjs +342 -0
- package/dist/chem/lewis.d.mts +79 -0
- package/dist/chem/lewis.mjs +163 -0
- package/dist/chem/molecular-orbital.d.mts +114 -0
- package/dist/chem/molecular-orbital.mjs +201 -0
- package/dist/chem/molecule.d.mts +154 -0
- package/dist/chem/molecule.mjs +282 -0
- package/dist/chem/nmr.d.mts +153 -0
- package/dist/chem/nmr.mjs +165 -0
- package/dist/chem/polymer.d.mts +65 -0
- package/dist/chem/polymer.mjs +179 -0
- package/dist/chem/redox.d.mts +138 -0
- package/dist/chem/redox.mjs +328 -0
- package/dist/chem/relations.d.mts +183 -0
- package/dist/chem/relations.mjs +299 -0
- package/dist/chem/skeleton.d.mts +51 -0
- package/dist/chem/skeleton.mjs +186 -0
- package/dist/chem/solution.d.mts +116 -0
- package/dist/chem/solution.mjs +186 -0
- package/dist/chem/species.d.mts +47 -0
- package/dist/chem/species.mjs +128 -0
- package/dist/chem/stereo.d.mts +108 -0
- package/dist/chem/stereo.mjs +286 -0
- package/dist/chem/stoichiometry.d.mts +95 -0
- package/dist/chem/stoichiometry.mjs +145 -0
- package/dist/chem/synthesis.d.mts +113 -0
- package/dist/chem/synthesis.mjs +281 -0
- package/dist/chem/vsepr.d.mts +80 -0
- package/dist/chem/vsepr.mjs +247 -0
- package/dist/circuit/ac.d.mts +163 -0
- package/dist/circuit/ac.mjs +452 -0
- package/dist/circuit/dc.d.mts +92 -0
- package/dist/circuit/dc.mjs +333 -0
- package/dist/circuit/index.d.mts +5 -92
- package/dist/circuit/index.mjs +5 -332
- package/dist/circuit/network.d.mts +99 -0
- package/dist/circuit/network.mjs +440 -0
- package/dist/circuit/topology.d.mts +102 -0
- package/dist/circuit/topology.mjs +143 -0
- package/dist/colour/absorption.d.mts +79 -0
- package/dist/colour/absorption.mjs +137 -0
- package/dist/colour/index.d.mts +6 -0
- package/dist/colour/index.mjs +7 -0
- package/dist/colour/name.d.mts +14 -0
- package/dist/colour/name.mjs +76 -0
- package/dist/colour/observer.d.mts +31 -0
- package/dist/colour/observer.mjs +46 -0
- package/dist/colour/spectrum.d.mts +61 -0
- package/dist/colour/spectrum.mjs +108 -0
- package/dist/colour/srgb.d.mts +49 -0
- package/dist/colour/srgb.mjs +108 -0
- package/dist/core/constants.d.mts +138 -0
- package/dist/core/constants.mjs +139 -0
- package/dist/core/index.d.mts +4 -1
- package/dist/core/index.mjs +4 -1
- package/dist/core/math.d.mts +26 -0
- package/dist/core/math.mjs +37 -0
- package/dist/core/vec3.d.mts +94 -0
- package/dist/core/vec3.mjs +123 -0
- package/dist/core/worked.d.mts +35 -0
- package/dist/core/worked.mjs +44 -0
- package/dist/electro/cell.d.mts +102 -0
- package/dist/electro/cell.mjs +170 -0
- package/dist/electro/conductivity.d.mts +157 -0
- package/dist/electro/conductivity.mjs +156 -0
- package/dist/electro/couples.d.mts +114 -0
- package/dist/electro/couples.mjs +334 -0
- package/dist/electro/electrolysis.d.mts +96 -0
- package/dist/electro/electrolysis.mjs +274 -0
- package/dist/electro/faraday.d.mts +150 -0
- package/dist/electro/faraday.mjs +118 -0
- package/dist/electro/index.d.mts +6 -0
- package/dist/electro/index.mjs +7 -0
- package/dist/finance/accounting/adjustments.d.mts +17 -0
- package/dist/finance/accounting/adjustments.mjs +202 -0
- package/dist/finance/accounting/analysis.d.mts +8 -0
- package/dist/finance/accounting/analysis.mjs +205 -0
- package/dist/finance/accounting/clubs.d.mts +21 -0
- package/dist/finance/accounting/clubs.mjs +217 -0
- package/dist/finance/accounting/company-statements.d.mts +7 -0
- package/dist/finance/accounting/company-statements.mjs +150 -0
- package/dist/finance/accounting/harness.d.mts +7 -0
- package/dist/finance/accounting/harness.mjs +45 -0
- package/dist/finance/accounting/index.d.mts +18 -0
- package/dist/finance/accounting/index.mjs +18 -0
- package/dist/finance/accounting/journalize.d.mts +16 -0
- package/dist/finance/accounting/journalize.mjs +128 -0
- package/dist/finance/accounting/manufacturing.d.mts +8 -0
- package/dist/finance/accounting/manufacturing.mjs +236 -0
- package/dist/finance/accounting/partnership.d.mts +7 -0
- package/dist/finance/accounting/partnership.mjs +152 -0
- package/dist/finance/accounting/period-case.d.mts +6 -0
- package/dist/finance/accounting/period-case.mjs +71 -0
- package/dist/finance/accounting/post.d.mts +9 -0
- package/dist/finance/accounting/post.mjs +84 -0
- package/dist/finance/accounting/precision.d.mts +9 -0
- package/dist/finance/accounting/precision.mjs +15 -0
- package/dist/finance/accounting/rules.d.mts +21 -0
- package/dist/finance/accounting/rules.mjs +37 -0
- package/dist/finance/accounting/solve.d.mts +28 -0
- package/dist/finance/accounting/solve.mjs +92 -0
- package/dist/finance/accounting/statements.d.mts +7 -0
- package/dist/finance/accounting/statements.mjs +142 -0
- package/dist/finance/accounting/types.d.mts +649 -0
- package/dist/finance/accounting/validate.d.mts +7 -0
- package/dist/finance/accounting/validate.mjs +67 -0
- package/dist/finance/accounting/verification.d.mts +24 -0
- package/dist/finance/accounting/verification.mjs +386 -0
- package/dist/finance/bizsim.d.mts +75 -0
- package/dist/finance/bizsim.mjs +296 -0
- package/dist/finance/index.d.mts +95 -0
- package/dist/finance/index.mjs +145 -0
- package/dist/finance/investment-math.d.mts +7 -0
- package/dist/finance/investment-math.mjs +18 -0
- package/dist/finance/management-accounting/budgeting.d.mts +7 -0
- package/dist/finance/management-accounting/budgeting.mjs +189 -0
- package/dist/finance/management-accounting/cash-budget.d.mts +7 -0
- package/dist/finance/management-accounting/cash-budget.mjs +154 -0
- package/dist/finance/management-accounting/costing.d.mts +8 -0
- package/dist/finance/management-accounting/costing.mjs +175 -0
- package/dist/finance/management-accounting/decisions.d.mts +8 -0
- package/dist/finance/management-accounting/decisions.mjs +164 -0
- package/dist/finance/management-accounting/index.d.mts +13 -0
- package/dist/finance/management-accounting/index.mjs +13 -0
- package/dist/finance/management-accounting/integrated-budget.d.mts +8 -0
- package/dist/finance/management-accounting/integrated-budget.mjs +67 -0
- package/dist/finance/management-accounting/inventory-costing.d.mts +7 -0
- package/dist/finance/management-accounting/inventory-costing.mjs +173 -0
- package/dist/finance/management-accounting/investment-appraisal.d.mts +7 -0
- package/dist/finance/management-accounting/investment-appraisal.mjs +148 -0
- package/dist/finance/management-accounting/management-case.d.mts +8 -0
- package/dist/finance/management-accounting/management-case.mjs +91 -0
- package/dist/finance/management-accounting/master-budget.d.mts +7 -0
- package/dist/finance/management-accounting/master-budget.mjs +137 -0
- package/dist/finance/management-accounting/production-costing.d.mts +8 -0
- package/dist/finance/management-accounting/production-costing.mjs +214 -0
- package/dist/finance/management-accounting/types.d.mts +644 -0
- package/dist/finance/management-accounting/variances.d.mts +7 -0
- package/dist/finance/management-accounting/variances.mjs +150 -0
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +5 -5
- package/dist/interaction/MovableDot.mjs +19 -0
- package/dist/interaction/useDraggable.mjs +24 -4
- package/dist/math/analysis.d.mts +61 -0
- package/dist/math/analysis.mjs +187 -0
- package/dist/math/answer-check.d.mts +50 -0
- package/dist/math/answer-check.mjs +106 -0
- package/dist/math/calculus.d.mts +17 -1
- package/dist/math/calculus.mjs +365 -2
- package/dist/math/complex.d.mts +93 -0
- package/dist/math/complex.mjs +239 -0
- package/dist/math/expr.d.mts +23 -0
- package/dist/math/expr.mjs +36 -0
- package/dist/math/index.d.mts +9 -23
- package/dist/math/index.mjs +9 -38
- package/dist/math/latex.mjs +10 -1
- package/dist/math/parse.mjs +1 -1
- package/dist/math/poly.d.mts +55 -0
- package/dist/math/poly.mjs +273 -0
- package/dist/math/rearrange.d.mts +14 -0
- package/dist/math/rearrange.mjs +125 -0
- package/dist/math/solve.d.mts +21 -0
- package/dist/math/solve.mjs +75 -0
- package/dist/networking/addressing.d.mts +26 -0
- package/dist/networking/addressing.mjs +33 -0
- package/dist/networking/cloud/access.d.mts +36 -0
- package/dist/networking/cloud/access.mjs +95 -0
- package/dist/networking/cloud/containers.d.mts +43 -0
- package/dist/networking/cloud/containers.mjs +54 -0
- package/dist/networking/cloud/edge.d.mts +17 -0
- package/dist/networking/cloud/edge.mjs +31 -0
- package/dist/networking/cloud/failure-domains.d.mts +19 -0
- package/dist/networking/cloud/failure-domains.mjs +22 -0
- package/dist/networking/cloud/geo-routing.d.mts +22 -0
- package/dist/networking/cloud/geo-routing.mjs +64 -0
- package/dist/networking/cloud/index.d.mts +18 -0
- package/dist/networking/cloud/index.mjs +19 -0
- package/dist/networking/cloud/inference-reliability.d.mts +30 -0
- package/dist/networking/cloud/inference-reliability.mjs +76 -0
- package/dist/networking/cloud/inference.d.mts +118 -0
- package/dist/networking/cloud/inference.mjs +179 -0
- package/dist/networking/cloud/infrastructure.d.mts +45 -0
- package/dist/networking/cloud/infrastructure.mjs +96 -0
- package/dist/networking/cloud/observability.d.mts +24 -0
- package/dist/networking/cloud/observability.mjs +34 -0
- package/dist/networking/cloud/orchestration.d.mts +32 -0
- package/dist/networking/cloud/orchestration.mjs +38 -0
- package/dist/networking/cloud/recovery.d.mts +21 -0
- package/dist/networking/cloud/recovery.mjs +27 -0
- package/dist/networking/cloud/releases.d.mts +42 -0
- package/dist/networking/cloud/releases.mjs +135 -0
- package/dist/networking/cloud/scaling.d.mts +25 -0
- package/dist/networking/cloud/scaling.mjs +65 -0
- package/dist/networking/cloud/secrets.d.mts +39 -0
- package/dist/networking/cloud/secrets.mjs +68 -0
- package/dist/networking/cloud/server-deployment.d.mts +29 -0
- package/dist/networking/cloud/server-deployment.mjs +72 -0
- package/dist/networking/cloud/storage.d.mts +16 -0
- package/dist/networking/cloud/storage.mjs +58 -0
- package/dist/networking/cloud/vpc.d.mts +18 -0
- package/dist/networking/cloud/vpc.mjs +100 -0
- package/dist/networking/delivery.d.mts +34 -0
- package/dist/networking/delivery.mjs +52 -0
- package/dist/networking/index.d.mts +47 -0
- package/dist/networking/index.mjs +47 -0
- package/dist/networking/ipv6.d.mts +28 -0
- package/dist/networking/ipv6.mjs +17 -0
- package/dist/networking/journeys.d.mts +83 -0
- package/dist/networking/journeys.mjs +161 -0
- package/dist/networking/models/airtime.d.mts +87 -0
- package/dist/networking/models/airtime.mjs +130 -0
- package/dist/networking/models/cellular.d.mts +157 -0
- package/dist/networking/models/cellular.mjs +252 -0
- package/dist/networking/models/encapsulation.d.mts +98 -0
- package/dist/networking/models/encapsulation.mjs +132 -0
- package/dist/networking/models/handover.d.mts +116 -0
- package/dist/networking/models/handover.mjs +209 -0
- package/dist/networking/models/handshake.d.mts +90 -0
- package/dist/networking/models/handshake.mjs +122 -0
- package/dist/networking/models/index.d.mts +14 -0
- package/dist/networking/models/index.mjs +15 -0
- package/dist/networking/models/media.d.mts +82 -0
- package/dist/networking/models/media.mjs +188 -0
- package/dist/networking/models/satellite.d.mts +86 -0
- package/dist/networking/models/satellite.mjs +121 -0
- package/dist/networking/models/spectrum.d.mts +83 -0
- package/dist/networking/models/spectrum.mjs +235 -0
- package/dist/networking/models/switch.d.mts +80 -0
- package/dist/networking/models/switch.mjs +155 -0
- package/dist/networking/models/vlan.d.mts +62 -0
- package/dist/networking/models/vlan.mjs +100 -0
- package/dist/networking/models/vpn.d.mts +66 -0
- package/dist/networking/models/vpn.mjs +125 -0
- package/dist/networking/models/wifi-privacy.d.mts +82 -0
- package/dist/networking/models/wifi-privacy.mjs +130 -0
- package/dist/networking/models/wifi.d.mts +240 -0
- package/dist/networking/models/wifi.mjs +562 -0
- package/dist/networking/nat-traversal.d.mts +24 -0
- package/dist/networking/nat-traversal.mjs +38 -0
- package/dist/networking/protocols.d.mts +17 -0
- package/dist/networking/protocols.mjs +57 -0
- package/dist/networking/queue.d.mts +12 -0
- package/dist/networking/queue.mjs +18 -0
- package/dist/networking/route-learning.d.mts +23 -0
- package/dist/networking/route-learning.mjs +43 -0
- package/dist/networking/routing.d.mts +11 -0
- package/dist/networking/routing.mjs +25 -0
- package/dist/networking/signals.d.mts +26 -0
- package/dist/networking/signals.mjs +35 -0
- package/dist/networking/subnet.d.mts +49 -0
- package/dist/networking/subnet.mjs +89 -0
- package/dist/networking/tcp-reliability.d.mts +24 -0
- package/dist/networking/tcp-reliability.mjs +38 -0
- package/dist/networking/topology/contract.d.mts +123 -0
- package/dist/networking/topology/index.d.mts +4 -0
- package/dist/networking/topology/index.mjs +4 -0
- package/dist/networking/topology/presets.d.mts +7 -0
- package/dist/networking/topology/presets.mjs +302 -0
- package/dist/networking/topology/simulation.d.mts +8 -0
- package/dist/networking/topology/simulation.mjs +179 -0
- package/dist/networking/wireless.d.mts +11 -0
- package/dist/networking/wireless.mjs +17 -0
- package/dist/optics/imaging.d.mts +218 -0
- package/dist/optics/imaging.mjs +276 -0
- package/dist/optics/index.d.mts +7 -0
- package/dist/optics/index.mjs +8 -0
- package/dist/optics/laws.d.mts +70 -0
- package/dist/optics/laws.mjs +195 -0
- package/dist/optics/prism.d.mts +148 -0
- package/dist/optics/prism.mjs +197 -0
- package/dist/optics/refraction.d.mts +201 -0
- package/dist/optics/refraction.mjs +270 -0
- package/dist/optics/wave-optics.d.mts +171 -0
- package/dist/optics/wave-optics.mjs +259 -0
- package/dist/physics/ac/index.d.mts +183 -0
- package/dist/physics/ac/index.mjs +203 -0
- package/dist/physics/dynamics/forces.d.mts +98 -0
- package/dist/physics/dynamics/forces.mjs +153 -0
- package/dist/physics/dynamics/friction.d.mts +76 -0
- package/dist/physics/dynamics/friction.mjs +119 -0
- package/dist/physics/dynamics/system.d.mts +68 -0
- package/dist/physics/dynamics/system.mjs +125 -0
- package/dist/physics/electrostatics/index.d.mts +169 -0
- package/dist/physics/electrostatics/index.mjs +249 -0
- package/dist/physics/energy/centre-of-mass.d.mts +115 -0
- package/dist/physics/energy/centre-of-mass.mjs +180 -0
- package/dist/physics/energy/conservation.d.mts +81 -0
- package/dist/physics/energy/conservation.mjs +129 -0
- package/dist/physics/energy/power.d.mts +90 -0
- package/dist/physics/energy/power.mjs +167 -0
- package/dist/physics/energy/spring.d.mts +73 -0
- package/dist/physics/energy/spring.mjs +129 -0
- package/dist/physics/energy/store.d.mts +63 -0
- package/dist/physics/energy/store.mjs +108 -0
- package/dist/physics/energy/theorem.d.mts +65 -0
- package/dist/physics/energy/theorem.mjs +113 -0
- package/dist/physics/energy/work.d.mts +114 -0
- package/dist/physics/energy/work.mjs +219 -0
- package/dist/physics/gravitation/body.d.mts +85 -0
- package/dist/physics/gravitation/body.mjs +168 -0
- package/dist/physics/gravitation/kepler.d.mts +127 -0
- package/dist/physics/gravitation/kepler.mjs +216 -0
- package/dist/physics/gravitation/law.d.mts +117 -0
- package/dist/physics/gravitation/law.mjs +175 -0
- package/dist/physics/gravitation/orbit.d.mts +119 -0
- package/dist/physics/gravitation/orbit.mjs +202 -0
- package/dist/physics/gravitation/potential.d.mts +114 -0
- package/dist/physics/gravitation/potential.mjs +202 -0
- package/dist/physics/gravitation/variation.d.mts +97 -0
- package/dist/physics/gravitation/variation.mjs +175 -0
- package/dist/physics/index.d.mts +35 -0
- package/dist/physics/index.mjs +36 -0
- package/dist/physics/integral-sweep.d.mts +63 -0
- package/dist/physics/integral-sweep.mjs +126 -0
- package/dist/physics/kinematics/equations.d.mts +47 -0
- package/dist/physics/kinematics/equations.mjs +126 -0
- package/dist/physics/kinematics/free-fall.d.mts +53 -0
- package/dist/physics/kinematics/free-fall.mjs +71 -0
- package/dist/physics/kinematics/given.d.mts +16 -0
- package/dist/physics/kinematics/given.mjs +55 -0
- package/dist/physics/kinematics/roots.d.mts +41 -0
- package/dist/physics/kinematics/roots.mjs +72 -0
- package/dist/physics/kinematics/solve.d.mts +52 -0
- package/dist/physics/kinematics/solve.mjs +172 -0
- package/dist/physics/magnetism/biot-savart.d.mts +161 -0
- package/dist/physics/magnetism/biot-savart.mjs +353 -0
- package/dist/physics/magnetism/flux.d.mts +94 -0
- package/dist/physics/magnetism/flux.mjs +313 -0
- package/dist/physics/magnetism/geomagnetism.d.mts +151 -0
- package/dist/physics/magnetism/geomagnetism.mjs +184 -0
- package/dist/physics/magnetism/index.d.mts +12 -0
- package/dist/physics/magnetism/index.mjs +74 -0
- package/dist/physics/magnetism/lorentz.d.mts +28 -0
- package/dist/physics/magnetism/lorentz.mjs +88 -0
- package/dist/physics/magnetism/magnetic-force.d.mts +102 -0
- package/dist/physics/magnetism/magnetic-force.mjs +169 -0
- package/dist/physics/momentum/collide.d.mts +77 -0
- package/dist/physics/momentum/collide.mjs +115 -0
- package/dist/physics/momentum/impulse.d.mts +89 -0
- package/dist/physics/momentum/impulse.mjs +159 -0
- package/dist/physics/momentum/planar.d.mts +60 -0
- package/dist/physics/momentum/planar.mjs +110 -0
- package/dist/physics/projectile.d.mts +107 -0
- package/dist/physics/projectile.mjs +185 -0
- package/dist/physics/rotation/angular.d.mts +85 -0
- package/dist/physics/rotation/angular.mjs +130 -0
- package/dist/physics/rotation/circular.d.mts +79 -0
- package/dist/physics/rotation/circular.mjs +131 -0
- package/dist/physics/rotation/inertia.d.mts +164 -0
- package/dist/physics/rotation/inertia.mjs +332 -0
- package/dist/physics/rotation/torque.d.mts +65 -0
- package/dist/physics/rotation/torque.mjs +134 -0
- package/dist/physics/vectors.d.mts +64 -0
- package/dist/physics/vectors.mjs +114 -0
- package/dist/primitives/Dot.d.mts +2 -15
- package/dist/primitives/Dot.mjs +6 -4
- package/dist/primitives/Grid.d.mts +33 -17
- package/dist/primitives/Grid.mjs +89 -17
- package/dist/primitives/Label.d.mts +1 -14
- package/dist/primitives/Label.mjs +3 -2
- package/dist/primitives/Lines.d.mts +4 -32
- package/dist/primitives/Lines.mjs +10 -8
- package/dist/primitives/Shapes.d.mts +5 -43
- package/dist/primitives/Shapes.mjs +12 -10
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.mjs +2 -2
- package/dist/primitives/props.mjs +31 -0
- package/dist/quantity/index.d.mts +119 -0
- package/dist/quantity/index.mjs +417 -0
- package/dist/relation/derive.d.mts +74 -0
- package/dist/relation/derive.mjs +73 -0
- package/dist/relation/index.d.mts +56 -0
- package/dist/relation/index.mjs +234 -0
- package/dist/scene/Scene.d.mts +6 -1
- package/dist/scene/Scene.mjs +15 -40
- package/dist/scene/evaluators.mjs +1 -1
- package/dist/sim/thermal.mjs +45 -20
- package/dist/thermo/calorimetry.d.mts +207 -0
- package/dist/thermo/calorimetry.mjs +345 -0
- package/dist/thermo/cycles.d.mts +142 -0
- package/dist/thermo/cycles.mjs +256 -0
- package/dist/thermo/entropy.d.mts +87 -0
- package/dist/thermo/entropy.mjs +140 -0
- package/dist/thermo/expansion.d.mts +130 -0
- package/dist/thermo/expansion.mjs +178 -0
- package/dist/thermo/gas.d.mts +197 -0
- package/dist/thermo/gas.mjs +279 -0
- package/dist/thermo/heat-transfer.d.mts +164 -0
- package/dist/thermo/heat-transfer.mjs +217 -0
- package/dist/thermo/index.d.mts +9 -82
- package/dist/thermo/index.mjs +9 -132
- package/dist/thermo/processes.d.mts +95 -0
- package/dist/thermo/processes.mjs +154 -0
- package/dist/thermo/temperature.d.mts +174 -0
- package/dist/thermo/temperature.mjs +236 -0
- package/dist/view/Stage.mjs +4 -11
- package/dist/waves/damping.d.mts +224 -0
- package/dist/waves/damping.mjs +317 -0
- package/dist/waves/doppler.d.mts +72 -0
- package/dist/waves/doppler.mjs +125 -0
- package/dist/waves/index.d.mts +10 -0
- package/dist/waves/index.mjs +10 -0
- package/dist/waves/pendulum.d.mts +130 -0
- package/dist/waves/pendulum.mjs +211 -0
- package/dist/waves/pitch.d.mts +125 -0
- package/dist/waves/pitch.mjs +158 -0
- package/dist/waves/shm.d.mts +170 -0
- package/dist/waves/shm.mjs +279 -0
- package/dist/waves/springs.d.mts +103 -0
- package/dist/waves/springs.mjs +176 -0
- package/dist/waves/superposition.d.mts +124 -0
- package/dist/waves/superposition.mjs +196 -0
- package/dist/waves/wave.d.mts +154 -0
- package/dist/waves/wave.mjs +178 -0
- package/package.json +103 -23
- package/styles.css +125 -8
- package/dist/assets/kit/index.mjs +0 -4
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFormula } from "./species.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/chem/equation.ts
|
|
4
|
+
/**
|
|
5
|
+
* Strip a trailing state symbol.
|
|
6
|
+
*
|
|
7
|
+
* `(aq)` looks exactly like a bracketed group to the formula reader, which would take the a and the
|
|
8
|
+
* q for elements. It has to come off before the formula is read, and only from the END, because
|
|
9
|
+
* `Fe(OH)3` has a bracketed group in the middle that must survive.
|
|
10
|
+
*/
|
|
11
|
+
const STATE_SYMBOL = /\((?:g|l|s|aq)\)\s*$/i;
|
|
12
|
+
const withoutState = (species) => species.replace(STATE_SYMBOL, "").trim();
|
|
13
|
+
const gcd = (a, b) => b === 0 ? Math.abs(a) : gcd(b, a % b);
|
|
14
|
+
function fraction(n, d = 1) {
|
|
15
|
+
if (d === 0) throw new Error("a fraction cannot have a denominator of zero");
|
|
16
|
+
const sign = d < 0 ? -1 : 1;
|
|
17
|
+
const divisor = gcd(n, d) || 1;
|
|
18
|
+
return {
|
|
19
|
+
n: sign * n / divisor,
|
|
20
|
+
d: sign * d / divisor
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const sub = (a, b) => fraction(a.n * b.d - b.n * a.d, a.d * b.d);
|
|
24
|
+
const mul = (a, b) => fraction(a.n * b.n, a.d * b.d);
|
|
25
|
+
const div = (a, b) => {
|
|
26
|
+
if (b.n === 0) throw new Error("cannot divide by zero while balancing");
|
|
27
|
+
return fraction(a.n * b.d, a.d * b.n);
|
|
28
|
+
};
|
|
29
|
+
const isZero = (f) => f.n === 0;
|
|
30
|
+
const ZERO = {
|
|
31
|
+
n: 0,
|
|
32
|
+
d: 1
|
|
33
|
+
};
|
|
34
|
+
const ONE = {
|
|
35
|
+
n: 1,
|
|
36
|
+
d: 1
|
|
37
|
+
};
|
|
38
|
+
/** Split "a + b -> c + d" into its two sides, accepting the arrows people actually type. */
|
|
39
|
+
function splitEquation(input) {
|
|
40
|
+
const parts = input.split(/->|=>|→|⟶|=/);
|
|
41
|
+
if (parts.length !== 2) throw new Error(`"${input}" is not an equation: it needs exactly one arrow, written as -> or = or →`);
|
|
42
|
+
const side = (text) => text.split(/\s+\+\s+/).map((s) => s.trim()).map((s) => s.replace(/^\d+(?=[A-Z([])/, "")).filter((s) => s !== "");
|
|
43
|
+
const left = side(parts[0]);
|
|
44
|
+
const right = side(parts[1]);
|
|
45
|
+
if (left.length === 0 || right.length === 0) throw new Error(`"${input}" has nothing on one side of the arrow`);
|
|
46
|
+
return {
|
|
47
|
+
left,
|
|
48
|
+
right
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Balance an equation, returning the smallest whole-number coefficients.
|
|
53
|
+
*
|
|
54
|
+
* `balance('Fe3O4 + CO -> Fe + CO2')`, `balance('C3H8 + O2 -> CO2 + H2O')`.
|
|
55
|
+
*
|
|
56
|
+
* Coefficients already written into the input are ignored: the whole point is to compute them, and
|
|
57
|
+
* accepting them would let a wrong one through unchallenged.
|
|
58
|
+
*/
|
|
59
|
+
function balance(input) {
|
|
60
|
+
const { left, right } = splitEquation(input);
|
|
61
|
+
const species = [...left, ...right];
|
|
62
|
+
const read = species.map((formula) => readFormula(withoutState(formula)));
|
|
63
|
+
const compositions = read.map((r) => r.counts);
|
|
64
|
+
const elements = [...new Set(compositions.flatMap((c) => c.map((e) => e.element)))];
|
|
65
|
+
/** Reactants count positive and products negative, so a balance sums to zero. */
|
|
66
|
+
const signed = (column, value) => fraction(column < left.length ? value : -value);
|
|
67
|
+
const rows = elements.map((element) => compositions.map((composition, column) => signed(column, composition.find((e) => e.element === element)?.count ?? 0)));
|
|
68
|
+
if (read.some((r) => r.charge !== 0)) rows.push(read.map((r, column) => signed(column, r.charge)));
|
|
69
|
+
const whole = clearFractions(nullSpaceVector(rows, species.length, input));
|
|
70
|
+
if (whole.some((value) => value <= 0)) throw new Error(`${input} cannot be balanced as written: the only solution needs a coefficient of zero or below, which usually means a species is on the wrong side or one is missing`);
|
|
71
|
+
return {
|
|
72
|
+
reactants: left.map((formula, i) => ({
|
|
73
|
+
formula,
|
|
74
|
+
coefficient: whole[i]
|
|
75
|
+
})),
|
|
76
|
+
products: right.map((formula, i) => ({
|
|
77
|
+
formula,
|
|
78
|
+
coefficient: whole[left.length + i]
|
|
79
|
+
})),
|
|
80
|
+
elements
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The one direction the element matrix sends to zero.
|
|
85
|
+
*
|
|
86
|
+
* Gaussian elimination to reduced row echelon form, then the single free column is set to one and
|
|
87
|
+
* the others read off. More than one free column means the equation has more than one independent
|
|
88
|
+
* balance, which is refused with that as the reason rather than by picking one of them.
|
|
89
|
+
*/
|
|
90
|
+
function nullSpaceVector(rows, columns, input) {
|
|
91
|
+
const matrix = rows.map((row) => [...row]);
|
|
92
|
+
const pivotOf = [];
|
|
93
|
+
let pivotRow = 0;
|
|
94
|
+
for (let column = 0; column < columns && pivotRow < matrix.length; column += 1) {
|
|
95
|
+
const found = matrix.findIndex((row, i) => i >= pivotRow && !isZero(row[column]));
|
|
96
|
+
if (found === -1) continue;
|
|
97
|
+
[matrix[pivotRow], matrix[found]] = [matrix[found], matrix[pivotRow]];
|
|
98
|
+
const pivot = matrix[pivotRow][column];
|
|
99
|
+
matrix[pivotRow] = matrix[pivotRow].map((value) => div(value, pivot));
|
|
100
|
+
for (let r = 0; r < matrix.length; r += 1) {
|
|
101
|
+
if (r === pivotRow || isZero(matrix[r][column])) continue;
|
|
102
|
+
const factor = matrix[r][column];
|
|
103
|
+
matrix[r] = matrix[r].map((value, c) => sub(value, mul(factor, matrix[pivotRow][c])));
|
|
104
|
+
}
|
|
105
|
+
pivotOf.push(column);
|
|
106
|
+
pivotRow += 1;
|
|
107
|
+
}
|
|
108
|
+
const free = [...Array(columns).keys()].filter((c) => !pivotOf.includes(c));
|
|
109
|
+
if (free.length === 0) throw new Error(`${input} cannot be balanced: no set of coefficients conserves every element, so a species is missing or a formula is wrong`);
|
|
110
|
+
if (free.length > 1) throw new Error(`${input} can be balanced in more than one independent way, which means it describes more than one reaction. Split it into separate equations.`);
|
|
111
|
+
const solution = Array(columns).fill(ZERO);
|
|
112
|
+
solution[free[0]] = ONE;
|
|
113
|
+
pivotOf.forEach((column, row) => {
|
|
114
|
+
solution[column] = mul(matrix[row][free[0]], {
|
|
115
|
+
n: -1,
|
|
116
|
+
d: 1
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
return solution;
|
|
120
|
+
}
|
|
121
|
+
/** Scale a set of fractions to the smallest whole numbers with the same ratios. */
|
|
122
|
+
function clearFractions(values) {
|
|
123
|
+
const lcm = (a, b) => Math.abs(a * b) / (gcd(a, b) || 1);
|
|
124
|
+
const denominator = values.reduce((total, f) => lcm(total, f.d), 1);
|
|
125
|
+
const scaled = values.map((f) => f.n * denominator / f.d);
|
|
126
|
+
const divisor = scaled.reduce((total, v) => gcd(total, v), 0) || 1;
|
|
127
|
+
return scaled.map((v) => v / divisor);
|
|
128
|
+
}
|
|
129
|
+
/** Write a balanced equation out, leaving a coefficient of one unwritten. */
|
|
130
|
+
function formatEquation(equation, arrow = "->") {
|
|
131
|
+
const side = (terms) => terms.map((t) => `${t.coefficient === 1 ? "" : t.coefficient}${t.formula}`).join(" + ");
|
|
132
|
+
return `${side(equation.reactants)} ${arrow} ${side(equation.products)}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* How many atoms of each element sit on each side.
|
|
136
|
+
*
|
|
137
|
+
* For showing the check rather than doing it: the balance is exact by construction, and a learner
|
|
138
|
+
* still has to see both columns match before they believe it.
|
|
139
|
+
*/
|
|
140
|
+
function atomTally(equation) {
|
|
141
|
+
const count = (terms, element) => terms.reduce((total, term) => {
|
|
142
|
+
const composition = readFormula(withoutState(term.formula)).counts;
|
|
143
|
+
return total + term.coefficient * (composition.find((e) => e.element === element)?.count ?? 0);
|
|
144
|
+
}, 0);
|
|
145
|
+
return equation.elements.map((element) => ({
|
|
146
|
+
element,
|
|
147
|
+
left: count(equation.reactants, element),
|
|
148
|
+
right: count(equation.products, element)
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Total charge on each side.
|
|
153
|
+
*
|
|
154
|
+
* Separate from the atom tally because it is a different conservation law, and because showing it
|
|
155
|
+
* beside the atoms is what makes an ionic equation make sense: the two sides of a precipitation
|
|
156
|
+
* carry the same charge as well as the same atoms, and a learner who only counts atoms will balance
|
|
157
|
+
* it wrongly and never see why.
|
|
158
|
+
*/
|
|
159
|
+
function chargeTally(equation) {
|
|
160
|
+
const total = (terms) => terms.reduce((sum, term) => sum + term.coefficient * readFormula(withoutState(term.formula)).charge, 0);
|
|
161
|
+
return {
|
|
162
|
+
left: total(equation.reactants),
|
|
163
|
+
right: total(equation.products)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
export { atomTally, balance, chargeTally, formatEquation, splitEquation };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/chem/formula.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Chemical formulae: what is in them, what they weigh, and what they weigh per element.
|
|
4
|
+
*
|
|
5
|
+
* ## One parser, because two would disagree
|
|
6
|
+
*
|
|
7
|
+
* A formula turns up in every quantitative question there is, and each of them needs the same three
|
|
8
|
+
* answers: which atoms, how many, and what that comes to. Writing a small reader for each caller is
|
|
9
|
+
* how a kernel ends up telling two different stories about Ca(NO3)2, so everything reads formulae
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* What it understands:
|
|
13
|
+
*
|
|
14
|
+
* Ca(NO3)2 groups in brackets, with a multiplier
|
|
15
|
+
* K4[Fe(CN)6] square brackets, nested
|
|
16
|
+
* CuSO4.5H2O a hydrate, written with a dot and a leading count
|
|
17
|
+
* MgCl2.KCl.6H2O a double salt, as many parts as it takes
|
|
18
|
+
*
|
|
19
|
+
* ## Masses come from the element table, and a formula that names nothing is refused
|
|
20
|
+
*
|
|
21
|
+
* Relative atomic masses are the IUPAC values the atomic engine already holds, so a mass computed
|
|
22
|
+
* here cannot disagree with one computed anywhere else. A symbol that is not an element stops the
|
|
23
|
+
* parse: `Xy2O3` is a typo, and a parser that skipped it would return a mass for a substance that
|
|
24
|
+
* does not exist, which is worse than no answer at all.
|
|
25
|
+
*
|
|
26
|
+
* ## The empirical formula is a search, not a division
|
|
27
|
+
*
|
|
28
|
+
* Dividing by the smallest mole count is only the first half. It lands on whole numbers for some
|
|
29
|
+
* compounds and on 1.5 or 1.33 for others, and the answer is to scale until every ratio is close to
|
|
30
|
+
* a whole number. {@link empiricalFormula} tries multipliers in order and takes the first that
|
|
31
|
+
* works, which is exactly the step a learner is told to do "by inspection" and is the step most
|
|
32
|
+
* often fudged.
|
|
33
|
+
*
|
|
34
|
+
* Pure: no React, no pixels.
|
|
35
|
+
*/
|
|
36
|
+
/** One element and how many of its atoms the formula contains. */
|
|
37
|
+
interface ElementCount {
|
|
38
|
+
readonly element: string;
|
|
39
|
+
readonly count: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The atoms in a formula, in the order they are written.
|
|
43
|
+
*
|
|
44
|
+
* Hydrates and double salts are split on the dot, each part parsed on its own and multiplied by its
|
|
45
|
+
* leading count, then added together. So CuSO4.5H2O has ten hydrogens, nine oxygens, one copper and
|
|
46
|
+
* one sulfur, which is what it weighs as.
|
|
47
|
+
*/
|
|
48
|
+
declare function parseFormula(input: string): ElementCount[];
|
|
49
|
+
/**
|
|
50
|
+
* Relative molecular mass, from the IUPAC atomic weights the element table holds.
|
|
51
|
+
*
|
|
52
|
+
* Called relative FORMULA mass for anything ionic, since sodium chloride has no molecules, and the
|
|
53
|
+
* arithmetic is identical either way.
|
|
54
|
+
*/
|
|
55
|
+
declare function molarMass(formula: string): number;
|
|
56
|
+
interface MassFraction {
|
|
57
|
+
readonly element: string;
|
|
58
|
+
/** Mass of this element in one formula unit. */
|
|
59
|
+
readonly mass: number;
|
|
60
|
+
/** Its share of the total, as a percentage. */
|
|
61
|
+
readonly percent: number;
|
|
62
|
+
}
|
|
63
|
+
/** What fraction of a compound's mass each element accounts for. */
|
|
64
|
+
declare function percentComposition(formula: string): MassFraction[];
|
|
65
|
+
/**
|
|
66
|
+
* The empirical formula implied by a composition.
|
|
67
|
+
*
|
|
68
|
+
* Takes percentages by mass, or any set of masses in the same unit: only the ratios matter, so the
|
|
69
|
+
* numbers do not have to add to a hundred and a set of measured masses works directly.
|
|
70
|
+
*
|
|
71
|
+
* The scaling step is the one that is usually waved through. Dividing by the smallest mole count
|
|
72
|
+
* can land on 1.5 or 1.33, and the ratios then have to be multiplied up until they are all close to
|
|
73
|
+
* whole numbers. That search is done here rather than left to inspection, and if no multiplier up
|
|
74
|
+
* to eight works the composition is refused rather than rounded into something tidy.
|
|
75
|
+
*/
|
|
76
|
+
declare function empiricalFormula(composition: Readonly<Record<string, number>>): ElementCount[];
|
|
77
|
+
/**
|
|
78
|
+
* How many empirical units make up the real molecule.
|
|
79
|
+
*
|
|
80
|
+
* Rounded, because the ratio of a measured molar mass to an exact empirical mass never comes out
|
|
81
|
+
* whole. Refuses when it is not close to one, since a value of 2.4 means the molar mass or the
|
|
82
|
+
* composition is wrong and multiplying by 2 would bury that.
|
|
83
|
+
*/
|
|
84
|
+
declare function unitsInMolecule(empirical: readonly ElementCount[], relativeMolecularMass: number): number;
|
|
85
|
+
/** The molecular formula: the empirical formula multiplied up to fit the measured mass. */
|
|
86
|
+
declare const molecularFormulaFor: (empirical: readonly ElementCount[], relativeMolecularMass: number) => ElementCount[];
|
|
87
|
+
/** Write counts back out as a formula, leaving a count of one unwritten. */
|
|
88
|
+
declare const formatFormula: (counts: readonly ElementCount[]) => string;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { ElementCount, MassFraction, empiricalFormula, formatFormula, molarMass, molecularFormulaFor, parseFormula, percentComposition, unitsInMolecule };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { elementBySymbol } from "../atomic/elements.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/chem/formula.ts
|
|
4
|
+
/**
|
|
5
|
+
* Chemical formulae: what is in them, what they weigh, and what they weigh per element.
|
|
6
|
+
*
|
|
7
|
+
* ## One parser, because two would disagree
|
|
8
|
+
*
|
|
9
|
+
* A formula turns up in every quantitative question there is, and each of them needs the same three
|
|
10
|
+
* answers: which atoms, how many, and what that comes to. Writing a small reader for each caller is
|
|
11
|
+
* how a kernel ends up telling two different stories about Ca(NO3)2, so everything reads formulae
|
|
12
|
+
* here.
|
|
13
|
+
*
|
|
14
|
+
* What it understands:
|
|
15
|
+
*
|
|
16
|
+
* Ca(NO3)2 groups in brackets, with a multiplier
|
|
17
|
+
* K4[Fe(CN)6] square brackets, nested
|
|
18
|
+
* CuSO4.5H2O a hydrate, written with a dot and a leading count
|
|
19
|
+
* MgCl2.KCl.6H2O a double salt, as many parts as it takes
|
|
20
|
+
*
|
|
21
|
+
* ## Masses come from the element table, and a formula that names nothing is refused
|
|
22
|
+
*
|
|
23
|
+
* Relative atomic masses are the IUPAC values the atomic engine already holds, so a mass computed
|
|
24
|
+
* here cannot disagree with one computed anywhere else. A symbol that is not an element stops the
|
|
25
|
+
* parse: `Xy2O3` is a typo, and a parser that skipped it would return a mass for a substance that
|
|
26
|
+
* does not exist, which is worse than no answer at all.
|
|
27
|
+
*
|
|
28
|
+
* ## The empirical formula is a search, not a division
|
|
29
|
+
*
|
|
30
|
+
* Dividing by the smallest mole count is only the first half. It lands on whole numbers for some
|
|
31
|
+
* compounds and on 1.5 or 1.33 for others, and the answer is to scale until every ratio is close to
|
|
32
|
+
* a whole number. {@link empiricalFormula} tries multipliers in order and takes the first that
|
|
33
|
+
* works, which is exactly the step a learner is told to do "by inspection" and is the step most
|
|
34
|
+
* often fudged.
|
|
35
|
+
*
|
|
36
|
+
* Pure: no React, no pixels.
|
|
37
|
+
*/
|
|
38
|
+
const isUpper = (c) => c >= "A" && c <= "Z";
|
|
39
|
+
const isLower = (c) => c >= "a" && c <= "z";
|
|
40
|
+
const isDigit = (c) => c >= "0" && c <= "9";
|
|
41
|
+
function readNumber(r) {
|
|
42
|
+
let digits = "";
|
|
43
|
+
while (r.at < r.text.length && isDigit(r.text[r.at])) digits += r.text[r.at++];
|
|
44
|
+
return digits === "" ? 1 : Number(digits);
|
|
45
|
+
}
|
|
46
|
+
const CLOSERS = {
|
|
47
|
+
"(": ")",
|
|
48
|
+
"[": "]"
|
|
49
|
+
};
|
|
50
|
+
/** Parse one group, stopping at a closing bracket or the end of the text. */
|
|
51
|
+
function readGroup(r, closer) {
|
|
52
|
+
const counts = /* @__PURE__ */ new Map();
|
|
53
|
+
const add = (element, n) => void counts.set(element, (counts.get(element) ?? 0) + n);
|
|
54
|
+
while (r.at < r.text.length) {
|
|
55
|
+
const c = r.text[r.at];
|
|
56
|
+
if (closer && c === closer) {
|
|
57
|
+
r.at += 1;
|
|
58
|
+
return counts;
|
|
59
|
+
}
|
|
60
|
+
if (c === "(" || c === "[") {
|
|
61
|
+
r.at += 1;
|
|
62
|
+
const inner = readGroup(r, CLOSERS[c]);
|
|
63
|
+
const multiplier = readNumber(r);
|
|
64
|
+
for (const [element, n] of inner) add(element, n * multiplier);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === ")" || c === "]") throw new Error(`cannot read "${r.text}": a closing bracket at position ${r.at} opens nothing`);
|
|
68
|
+
if (!isUpper(c)) throw new Error(`cannot read "${r.text}": expected an element symbol at position ${r.at}, found "${c}"`);
|
|
69
|
+
let symbol = r.text[r.at++];
|
|
70
|
+
if (r.at < r.text.length && isLower(r.text[r.at])) symbol += r.text[r.at++];
|
|
71
|
+
if (!elementBySymbol(symbol)) throw new Error(`cannot read "${r.text}": "${symbol}" is not an element`);
|
|
72
|
+
add(symbol, readNumber(r));
|
|
73
|
+
}
|
|
74
|
+
if (closer) throw new Error(`cannot read "${r.text}": a bracket is never closed`);
|
|
75
|
+
return counts;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The atoms in a formula, in the order they are written.
|
|
79
|
+
*
|
|
80
|
+
* Hydrates and double salts are split on the dot, each part parsed on its own and multiplied by its
|
|
81
|
+
* leading count, then added together. So CuSO4.5H2O has ten hydrogens, nine oxygens, one copper and
|
|
82
|
+
* one sulfur, which is what it weighs as.
|
|
83
|
+
*/
|
|
84
|
+
function parseFormula(input) {
|
|
85
|
+
const text = input.replace(/\s+/g, "");
|
|
86
|
+
if (text === "") throw new Error("an empty formula has no composition");
|
|
87
|
+
const total = /* @__PURE__ */ new Map();
|
|
88
|
+
text.split(".").forEach((part, index) => {
|
|
89
|
+
if (part === "") throw new Error(`cannot read "${input}": an empty part between dots`);
|
|
90
|
+
const r = {
|
|
91
|
+
text: part,
|
|
92
|
+
at: 0
|
|
93
|
+
};
|
|
94
|
+
const multiplier = index === 0 ? 1 : readNumber(r);
|
|
95
|
+
for (const [element, n] of readGroup(r)) total.set(element, (total.get(element) ?? 0) + n * multiplier);
|
|
96
|
+
});
|
|
97
|
+
return [...total].map(([element, count]) => ({
|
|
98
|
+
element,
|
|
99
|
+
count
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Relative molecular mass, from the IUPAC atomic weights the element table holds.
|
|
104
|
+
*
|
|
105
|
+
* Called relative FORMULA mass for anything ionic, since sodium chloride has no molecules, and the
|
|
106
|
+
* arithmetic is identical either way.
|
|
107
|
+
*/
|
|
108
|
+
function molarMass(formula) {
|
|
109
|
+
return parseFormula(formula).reduce((total, { element, count }) => total + elementBySymbol(element).atomicWeight * count, 0);
|
|
110
|
+
}
|
|
111
|
+
/** What fraction of a compound's mass each element accounts for. */
|
|
112
|
+
function percentComposition(formula) {
|
|
113
|
+
const total = molarMass(formula);
|
|
114
|
+
return parseFormula(formula).map(({ element, count }) => {
|
|
115
|
+
const mass = elementBySymbol(element).atomicWeight * count;
|
|
116
|
+
return {
|
|
117
|
+
element,
|
|
118
|
+
mass,
|
|
119
|
+
percent: mass / total * 100
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* How close a scaled ratio has to be to a whole number before it counts as one.
|
|
125
|
+
*
|
|
126
|
+
* Fixed, and deliberately not loosened as the multiplier grows. Scaling the tolerance with the
|
|
127
|
+
* multiplier made every larger multiplier more forgiving than the last, so a composition that fits
|
|
128
|
+
* nothing would always find some multiple that looked close enough, and the refusal below could
|
|
129
|
+
* never fire.
|
|
130
|
+
*/
|
|
131
|
+
const WHOLE_TOLERANCE = .1;
|
|
132
|
+
/** Multipliers tried when the first division does not land on whole numbers. */
|
|
133
|
+
const MULTIPLIERS = [
|
|
134
|
+
1,
|
|
135
|
+
2,
|
|
136
|
+
3,
|
|
137
|
+
4,
|
|
138
|
+
5,
|
|
139
|
+
6,
|
|
140
|
+
8
|
|
141
|
+
];
|
|
142
|
+
/**
|
|
143
|
+
* The empirical formula implied by a composition.
|
|
144
|
+
*
|
|
145
|
+
* Takes percentages by mass, or any set of masses in the same unit: only the ratios matter, so the
|
|
146
|
+
* numbers do not have to add to a hundred and a set of measured masses works directly.
|
|
147
|
+
*
|
|
148
|
+
* The scaling step is the one that is usually waved through. Dividing by the smallest mole count
|
|
149
|
+
* can land on 1.5 or 1.33, and the ratios then have to be multiplied up until they are all close to
|
|
150
|
+
* whole numbers. That search is done here rather than left to inspection, and if no multiplier up
|
|
151
|
+
* to eight works the composition is refused rather than rounded into something tidy.
|
|
152
|
+
*/
|
|
153
|
+
function empiricalFormula(composition) {
|
|
154
|
+
const entries = Object.entries(composition).filter(([, amount]) => amount > 0);
|
|
155
|
+
if (entries.length === 0) throw new Error("a composition with nothing in it has no formula");
|
|
156
|
+
const moles = entries.map(([element, amount]) => {
|
|
157
|
+
const record = elementBySymbol(element);
|
|
158
|
+
if (!record) throw new Error(`"${element}" is not an element`);
|
|
159
|
+
return {
|
|
160
|
+
element,
|
|
161
|
+
moles: amount / record.atomicWeight
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
const smallest = Math.min(...moles.map((m) => m.moles));
|
|
165
|
+
const ratios = moles.map((m) => m.moles / smallest);
|
|
166
|
+
for (const multiplier of MULTIPLIERS) {
|
|
167
|
+
const scaled = ratios.map((r) => r * multiplier);
|
|
168
|
+
if (scaled.every((value) => Math.abs(value - Math.round(value)) <= WHOLE_TOLERANCE)) return moles.map((m, i) => ({
|
|
169
|
+
element: m.element,
|
|
170
|
+
count: Math.round(scaled[i])
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`this composition does not reduce to a whole-number ratio: ${ratios.map((r, i) => `${moles[i].element} ${r.toFixed(3)}`).join(", ")}. Check the percentages add up and that no element is missing.`);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* How many empirical units make up the real molecule.
|
|
177
|
+
*
|
|
178
|
+
* Rounded, because the ratio of a measured molar mass to an exact empirical mass never comes out
|
|
179
|
+
* whole. Refuses when it is not close to one, since a value of 2.4 means the molar mass or the
|
|
180
|
+
* composition is wrong and multiplying by 2 would bury that.
|
|
181
|
+
*/
|
|
182
|
+
function unitsInMolecule(empirical, relativeMolecularMass) {
|
|
183
|
+
const empiricalMass = empirical.reduce((total, { element, count }) => total + elementBySymbol(element).atomicWeight * count, 0);
|
|
184
|
+
const ratio = relativeMolecularMass / empiricalMass;
|
|
185
|
+
const whole = Math.round(ratio);
|
|
186
|
+
if (whole < 1 || Math.abs(ratio - whole) > .1) throw new Error(`a relative molecular mass of ${relativeMolecularMass} is ${ratio.toFixed(2)} times the empirical mass of ${empiricalMass.toFixed(1)}, which is not a whole number of units`);
|
|
187
|
+
return whole;
|
|
188
|
+
}
|
|
189
|
+
/** The molecular formula: the empirical formula multiplied up to fit the measured mass. */
|
|
190
|
+
const molecularFormulaFor = (empirical, relativeMolecularMass) => {
|
|
191
|
+
const n = unitsInMolecule(empirical, relativeMolecularMass);
|
|
192
|
+
return empirical.map(({ element, count }) => ({
|
|
193
|
+
element,
|
|
194
|
+
count: count * n
|
|
195
|
+
}));
|
|
196
|
+
};
|
|
197
|
+
/** Write counts back out as a formula, leaving a count of one unwritten. */
|
|
198
|
+
const formatFormula = (counts) => counts.map(({ element, count }) => `${element}${count === 1 ? "" : count}`).join("");
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
export { empiricalFormula, formatFormula, molarMass, molecularFormulaFor, parseFormula, percentComposition, unitsInMolecule };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Structure } from "./molecule.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/chem/functional-groups.d.ts
|
|
4
|
+
type GroupKind = 'alkene' | 'alkyne' | 'arene' | 'alcohol' | 'halogenoalkane' | 'aldehyde' | 'ketone' | 'carboxylic-acid' | 'ester' | 'acyl-chloride' | 'amide' | 'amine' | 'nitrile';
|
|
5
|
+
interface FoundGroup {
|
|
6
|
+
readonly kind: GroupKind;
|
|
7
|
+
/** The atoms that make up the group, so a figure can highlight exactly these. */
|
|
8
|
+
readonly atoms: readonly number[];
|
|
9
|
+
/**
|
|
10
|
+
* Primary, secondary or tertiary, where the syllabus distinguishes them.
|
|
11
|
+
*
|
|
12
|
+
* Alcohols and halogenoalkanes, because the class decides the mechanism, and amines, because it
|
|
13
|
+
* decides the basicity.
|
|
14
|
+
*/
|
|
15
|
+
readonly klass?: 'primary' | 'secondary' | 'tertiary';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Every functional group in the structure.
|
|
19
|
+
*
|
|
20
|
+
* An atom may appear in more than one group only when the chemistry really has two: an amino acid
|
|
21
|
+
* genuinely has both a carboxylic acid and an amine, and reporting one would lose the point of the
|
|
22
|
+
* molecule. What never happens is the same atoms being reported twice under different names, which
|
|
23
|
+
* is what the carbonyl classification exists to prevent.
|
|
24
|
+
*/
|
|
25
|
+
declare function functionalGroups(structure: Structure): FoundGroup[];
|
|
26
|
+
/** Does the structure contain this group? The question a reagent test actually asks. */
|
|
27
|
+
declare const hasGroup: (structure: Structure, kind: GroupKind) => boolean;
|
|
28
|
+
/** Group kinds present, each once, in the order they were found. */
|
|
29
|
+
declare const groupKinds: (structure: Structure) => GroupKind[];
|
|
30
|
+
//#endregion
|
|
31
|
+
export { FoundGroup, GroupKind, functionalGroups, groupKinds, hasGroup };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { bondsAt, implicitHydrogens, otherEnd } from "./molecule.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/chem/functional-groups.ts
|
|
4
|
+
/**
|
|
5
|
+
* functional-groups, found in a structure rather than declared beside it.
|
|
6
|
+
*
|
|
7
|
+
* WHAT THIS IS FOR. Every lab that highlights a functional group currently does it by listing vertex
|
|
8
|
+
* indices, which means the highlight and the structure are two independent claims and nothing checks
|
|
9
|
+
* that they agree. Worse, it means a learner is shown "here is the carbonyl" rather than being
|
|
10
|
+
* taught what makes a carbonyl a carbonyl, because the recognition was done by the author.
|
|
11
|
+
*
|
|
12
|
+
* Perception closes both. A group is a small pattern in the graph, and finding it is the same work
|
|
13
|
+
* the learner is being asked to do.
|
|
14
|
+
*
|
|
15
|
+
* ## Why the order matters
|
|
16
|
+
*
|
|
17
|
+
* Several groups share a C=O and are told apart only by what else is on that carbon. An ester, an
|
|
18
|
+
* acid, an amide, an acyl chloride, an aldehyde and a ketone all contain one. So the carbonyl is
|
|
19
|
+
* classified ONCE, by looking at its other neighbours, and the more specific answer wins: a
|
|
20
|
+
* structure with a C=O and an O-H on the same carbon is a carboxylic acid and is not reported as a
|
|
21
|
+
* ketone with an alcohol attached.
|
|
22
|
+
*
|
|
23
|
+
* That ordering is not an implementation detail. It is exactly the reasoning an exam question wants,
|
|
24
|
+
* and stating it as a sequence of tests is what makes it teachable.
|
|
25
|
+
*
|
|
26
|
+
* Pure: no React, no pixels.
|
|
27
|
+
*/
|
|
28
|
+
const isC = (s, id) => s.atoms[id].element === "C";
|
|
29
|
+
/** Carbons attached to this atom. */
|
|
30
|
+
const carbonNeighbours = (s, id) => bondsAt(s, id).map((b) => otherEnd(b, id)).filter((n) => isC(s, n));
|
|
31
|
+
/** An oxygen bonded to exactly one heavy atom, so its other bond is to hydrogen. */
|
|
32
|
+
const isHydroxyl = (s, id) => s.atoms[id].element === "O" && bondsAt(s, id).length === 1 && implicitHydrogens(s, id) === 1;
|
|
33
|
+
/** Primary, secondary or tertiary, from the carbons attached to the carbon bearing the group. */
|
|
34
|
+
function classify(s, carbon, exclude) {
|
|
35
|
+
const others = carbonNeighbours(s, carbon).filter((n) => n !== exclude).length;
|
|
36
|
+
return others <= 1 ? "primary" : others === 2 ? "secondary" : "tertiary";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Classify one carbonyl carbon by what else is attached to it.
|
|
40
|
+
*
|
|
41
|
+
* The whole of the carbonyl family, in the order an exam question works through it: look for the
|
|
42
|
+
* second oxygen first, because that is what separates the acid and the ester from everything else.
|
|
43
|
+
*/
|
|
44
|
+
function carbonylKind(s, carbon, oxygen) {
|
|
45
|
+
const attached = bondsAt(s, carbon).map((b) => otherEnd(b, carbon)).filter((n) => n !== oxygen);
|
|
46
|
+
const hydrogens = implicitHydrogens(s, carbon);
|
|
47
|
+
const hydroxyl = attached.find((n) => isHydroxyl(s, n));
|
|
48
|
+
if (hydroxyl !== void 0) return {
|
|
49
|
+
kind: "carboxylic-acid",
|
|
50
|
+
atoms: [
|
|
51
|
+
carbon,
|
|
52
|
+
oxygen,
|
|
53
|
+
hydroxyl
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
const etherO = attached.find((n) => s.atoms[n].element === "O" && bondsAt(s, n).length === 2);
|
|
57
|
+
if (etherO !== void 0) return {
|
|
58
|
+
kind: "ester",
|
|
59
|
+
atoms: [
|
|
60
|
+
carbon,
|
|
61
|
+
oxygen,
|
|
62
|
+
etherO
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
const chlorine = attached.find((n) => s.atoms[n].element === "Cl");
|
|
66
|
+
if (chlorine !== void 0) return {
|
|
67
|
+
kind: "acyl-chloride",
|
|
68
|
+
atoms: [
|
|
69
|
+
carbon,
|
|
70
|
+
oxygen,
|
|
71
|
+
chlorine
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
const nitrogen = attached.find((n) => s.atoms[n].element === "N");
|
|
75
|
+
if (nitrogen !== void 0) return {
|
|
76
|
+
kind: "amide",
|
|
77
|
+
atoms: [
|
|
78
|
+
carbon,
|
|
79
|
+
oxygen,
|
|
80
|
+
nitrogen
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
const carbons = attached.filter((n) => isC(s, n)).length;
|
|
84
|
+
if (hydrogens >= 1 || carbons <= 1) return {
|
|
85
|
+
kind: "aldehyde",
|
|
86
|
+
atoms: [carbon, oxygen]
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
kind: "ketone",
|
|
90
|
+
atoms: [carbon, oxygen]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every functional group in the structure.
|
|
95
|
+
*
|
|
96
|
+
* An atom may appear in more than one group only when the chemistry really has two: an amino acid
|
|
97
|
+
* genuinely has both a carboxylic acid and an amine, and reporting one would lose the point of the
|
|
98
|
+
* molecule. What never happens is the same atoms being reported twice under different names, which
|
|
99
|
+
* is what the carbonyl classification exists to prevent.
|
|
100
|
+
*/
|
|
101
|
+
function functionalGroups(structure) {
|
|
102
|
+
const found = [];
|
|
103
|
+
const carbonylOxygens = /* @__PURE__ */ new Set();
|
|
104
|
+
const groupedO = /* @__PURE__ */ new Set();
|
|
105
|
+
const groupedN = /* @__PURE__ */ new Set();
|
|
106
|
+
for (const bond of structure.bonds) {
|
|
107
|
+
if (bond.order !== 2 || bond.aromatic) continue;
|
|
108
|
+
const pair = [bond.a, bond.b];
|
|
109
|
+
const carbon = pair.find((n) => isC(structure, n));
|
|
110
|
+
const oxygen = pair.find((n) => structure.atoms[n].element === "O");
|
|
111
|
+
if (carbon === void 0 || oxygen === void 0) continue;
|
|
112
|
+
const group = carbonylKind(structure, carbon, oxygen);
|
|
113
|
+
if (!group) continue;
|
|
114
|
+
found.push(group);
|
|
115
|
+
carbonylOxygens.add(oxygen);
|
|
116
|
+
for (const id of group.atoms) {
|
|
117
|
+
if (structure.atoms[id].element === "O") groupedO.add(id);
|
|
118
|
+
if (structure.atoms[id].element === "N") groupedN.add(id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const bond of structure.bonds) {
|
|
122
|
+
if (bond.aromatic) continue;
|
|
123
|
+
const bothCarbon = isC(structure, bond.a) && isC(structure, bond.b);
|
|
124
|
+
if (bothCarbon && bond.order === 2) found.push({
|
|
125
|
+
kind: "alkene",
|
|
126
|
+
atoms: [bond.a, bond.b]
|
|
127
|
+
});
|
|
128
|
+
if (bothCarbon && bond.order === 3) found.push({
|
|
129
|
+
kind: "alkyne",
|
|
130
|
+
atoms: [bond.a, bond.b]
|
|
131
|
+
});
|
|
132
|
+
if (bond.order === 3 && !bothCarbon) {
|
|
133
|
+
const carbon = [bond.a, bond.b].find((n) => isC(structure, n));
|
|
134
|
+
const nitrogen = [bond.a, bond.b].find((n) => structure.atoms[n].element === "N");
|
|
135
|
+
if (carbon !== void 0 && nitrogen !== void 0) {
|
|
136
|
+
found.push({
|
|
137
|
+
kind: "nitrile",
|
|
138
|
+
atoms: [carbon, nitrogen]
|
|
139
|
+
});
|
|
140
|
+
groupedN.add(nitrogen);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (structure.atoms.some((a) => a.aromatic)) found.push({
|
|
145
|
+
kind: "arene",
|
|
146
|
+
atoms: structure.atoms.filter((a) => a.aromatic).map((a) => a.id)
|
|
147
|
+
});
|
|
148
|
+
for (const atom of structure.atoms) {
|
|
149
|
+
if (atom.element === "O" && !groupedO.has(atom.id) && isHydroxyl(structure, atom.id)) {
|
|
150
|
+
const carbon = carbonNeighbours(structure, atom.id)[0];
|
|
151
|
+
if (carbon !== void 0) found.push({
|
|
152
|
+
kind: "alcohol",
|
|
153
|
+
atoms: [carbon, atom.id],
|
|
154
|
+
klass: classify(structure, carbon, atom.id)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if ([
|
|
158
|
+
"F",
|
|
159
|
+
"Cl",
|
|
160
|
+
"Br",
|
|
161
|
+
"I"
|
|
162
|
+
].includes(atom.element)) {
|
|
163
|
+
const carbon = carbonNeighbours(structure, atom.id)[0];
|
|
164
|
+
if (carbon !== void 0 && !found.some((g) => g.kind === "acyl-chloride" && g.atoms.includes(atom.id))) found.push({
|
|
165
|
+
kind: "halogenoalkane",
|
|
166
|
+
atoms: [carbon, atom.id],
|
|
167
|
+
klass: classify(structure, carbon, atom.id)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (atom.element === "N" && !groupedN.has(atom.id)) {
|
|
171
|
+
const carbons = carbonNeighbours(structure, atom.id);
|
|
172
|
+
if (carbons.length >= 1) found.push({
|
|
173
|
+
kind: "amine",
|
|
174
|
+
atoms: [atom.id, ...carbons],
|
|
175
|
+
klass: carbons.length === 1 ? "primary" : carbons.length === 2 ? "secondary" : "tertiary"
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return found;
|
|
180
|
+
}
|
|
181
|
+
/** Does the structure contain this group? The question a reagent test actually asks. */
|
|
182
|
+
const hasGroup = (structure, kind) => functionalGroups(structure).some((g) => g.kind === kind);
|
|
183
|
+
/** Group kinds present, each once, in the order they were found. */
|
|
184
|
+
const groupKinds = (structure) => [...new Set(functionalGroups(structure).map((g) => g.kind))];
|
|
185
|
+
|
|
186
|
+
//#endregion
|
|
187
|
+
export { functionalGroups, groupKinds, hasGroup };
|