@classytic/stage 0.3.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/dist/_virtual/_rolldown/runtime.mjs +18 -0
- 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/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 +3 -1
- package/dist/core/index.mjs +3 -1
- 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 +21 -39
- package/dist/finance/bizsim.mjs +276 -97
- package/dist/finance/index.d.mts +20 -43
- package/dist/finance/index.mjs +19 -77
- 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 +6 -6
- package/dist/index.mjs +1 -1
- 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 +4 -9
- package/dist/math/calculus.mjs +216 -7
- 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 +8 -22
- package/dist/math/index.mjs +8 -37
- package/dist/math/latex.mjs +2 -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/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/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/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 +74 -2
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//#region src/networking/models/encapsulation.ts
|
|
2
|
+
/** Outermost last: this is the order the headers are ADDED going down the stack. */
|
|
3
|
+
const HEADERS = [
|
|
4
|
+
{
|
|
5
|
+
kind: "application",
|
|
6
|
+
protocol: "HTTP",
|
|
7
|
+
bytes: 0,
|
|
8
|
+
addressing: null,
|
|
9
|
+
answers: "What is being asked for. This is the message itself, not a wrapper."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
kind: "transport",
|
|
13
|
+
protocol: "TCP",
|
|
14
|
+
bytes: 20,
|
|
15
|
+
addressing: "port",
|
|
16
|
+
answers: "Which program on the machine, and where this piece fits in the stream."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
kind: "network",
|
|
20
|
+
protocol: "IPv4",
|
|
21
|
+
bytes: 20,
|
|
22
|
+
addressing: "IP address",
|
|
23
|
+
answers: "Which machine, anywhere. Set once by the sender and carried the whole way."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
kind: "data-link",
|
|
27
|
+
protocol: "Ethernet",
|
|
28
|
+
bytes: 18,
|
|
29
|
+
trailerBytes: 4,
|
|
30
|
+
addressing: "MAC address",
|
|
31
|
+
answers: "Which box on THIS wire takes it next. Replaced at every routed hop."
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
const OVERHEAD_BYTES = HEADERS.reduce((sum, header) => sum + header.bytes, 0);
|
|
35
|
+
/** Wrap a payload in the first `depth` headers. Full depth is the frame that goes on the wire. */
|
|
36
|
+
function encapsulate(payloadBytes, depth = HEADERS.length) {
|
|
37
|
+
const applied = HEADERS.slice(0, Math.max(0, Math.min(depth, HEADERS.length)));
|
|
38
|
+
const overheadBytes = applied.reduce((sum, header) => sum + header.bytes, 0);
|
|
39
|
+
const onWireBytes = payloadBytes + overheadBytes;
|
|
40
|
+
return {
|
|
41
|
+
payloadBytes,
|
|
42
|
+
headers: applied,
|
|
43
|
+
depth: applied.length,
|
|
44
|
+
onWireBytes,
|
|
45
|
+
overheadBytes,
|
|
46
|
+
overheadShare: onWireBytes === 0 ? 0 : overheadBytes / onWireBytes
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Unwrap one layer. The payload is never touched, which is the point. */
|
|
50
|
+
const decapsulate = (state) => encapsulate(state.payloadBytes, Math.max(0, state.depth - 1));
|
|
51
|
+
/** How deep a device opens the frame before it can do its job. */
|
|
52
|
+
const opensTo = (kind) => kind === "switch" ? "data-link" : kind === "router" ? "network" : "application";
|
|
53
|
+
/**
|
|
54
|
+
* Split a path at its layer-3 boundaries. A switch forwards a frame without terminating it, so it
|
|
55
|
+
* does not start a new segment. A router does, because it builds a brand new frame for the next
|
|
56
|
+
* wire. The number of segments is therefore routers + 1, however many switches are in the way.
|
|
57
|
+
*/
|
|
58
|
+
function segmentsOf(path) {
|
|
59
|
+
const boundaries = path.filter((node) => node.kind !== "switch");
|
|
60
|
+
const segments = [];
|
|
61
|
+
for (let index = 0; index < boundaries.length - 1; index++) {
|
|
62
|
+
const from = boundaries[index];
|
|
63
|
+
const to = boundaries[index + 1];
|
|
64
|
+
const through = path.slice(path.indexOf(from) + 1, path.indexOf(to)).filter((n) => n.kind === "switch");
|
|
65
|
+
segments.push({
|
|
66
|
+
from,
|
|
67
|
+
to,
|
|
68
|
+
fromMac: from.mac,
|
|
69
|
+
toMac: to.mac,
|
|
70
|
+
through
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return segments;
|
|
74
|
+
}
|
|
75
|
+
function frameOn(segment, source, destination) {
|
|
76
|
+
const rebuilt = segment.from.kind === "router";
|
|
77
|
+
return {
|
|
78
|
+
segment,
|
|
79
|
+
mac: {
|
|
80
|
+
from: segment.fromMac,
|
|
81
|
+
to: segment.toMac
|
|
82
|
+
},
|
|
83
|
+
ip: {
|
|
84
|
+
from: source.ip ?? "",
|
|
85
|
+
to: destination.ip ?? ""
|
|
86
|
+
},
|
|
87
|
+
note: rebuilt ? `${segment.from.name} threw away the frame it received and built a new one for this wire. Same packet inside, new MAC addresses outside.` : `${segment.from.name} addresses the frame to ${segment.to.name}, the next box on this wire, not to the far end.`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Every frame the message travels in, end to end. */
|
|
91
|
+
const journey = (path) => {
|
|
92
|
+
const source = path[0];
|
|
93
|
+
const destination = path[path.length - 1];
|
|
94
|
+
return segmentsOf(path).map((segment) => frameOn(segment, source, destination));
|
|
95
|
+
};
|
|
96
|
+
const DEFAULT_PATH = [
|
|
97
|
+
{
|
|
98
|
+
id: "laptop",
|
|
99
|
+
name: "Laptop",
|
|
100
|
+
kind: "host",
|
|
101
|
+
mac: "AA:01",
|
|
102
|
+
ip: "10.1.0.20"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "switch",
|
|
106
|
+
name: "Switch",
|
|
107
|
+
kind: "switch",
|
|
108
|
+
mac: "SW:00"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "r1",
|
|
112
|
+
name: "Router 1",
|
|
113
|
+
kind: "router",
|
|
114
|
+
mac: "BB:11"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "r2",
|
|
118
|
+
name: "Router 2",
|
|
119
|
+
kind: "router",
|
|
120
|
+
mac: "CC:22"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "server",
|
|
124
|
+
name: "Server",
|
|
125
|
+
kind: "host",
|
|
126
|
+
mac: "DD:33",
|
|
127
|
+
ip: "10.9.0.7"
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
export { DEFAULT_PATH, HEADERS, OVERHEAD_BYTES, decapsulate, encapsulate, frameOn, journey, opensTo, segmentsOf };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Terrain } from "./cellular.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/networking/models/handover.d.ts
|
|
4
|
+
/** A mast beside a straight road, positioned by how far along the road it stands. */
|
|
5
|
+
interface Mast {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Distance along the road, in km. */
|
|
9
|
+
atKm: number;
|
|
10
|
+
/** Perpendicular offset from the road, in km. Keeps the signal finite as the car passes. */
|
|
11
|
+
offsetKm?: number;
|
|
12
|
+
}
|
|
13
|
+
interface DriveOptions {
|
|
14
|
+
/** How much better a neighbour must be before the call moves, in dB. The whole lesson. */
|
|
15
|
+
marginDb: number;
|
|
16
|
+
/** Where the drive starts along the road, in km. Defaults to the beginning. */
|
|
17
|
+
fromKm?: number;
|
|
18
|
+
/** Where it ends, in km. */
|
|
19
|
+
roadKm: number;
|
|
20
|
+
megahertz: number;
|
|
21
|
+
terrain: Terrain;
|
|
22
|
+
/** Standard deviation of the shadow fading, in dB. 6 is a normal urban figure. */
|
|
23
|
+
shadowDb?: number;
|
|
24
|
+
/** How far the shadowing persists, in km. */
|
|
25
|
+
correlationKm?: number;
|
|
26
|
+
stepKm?: number;
|
|
27
|
+
seed?: number;
|
|
28
|
+
}
|
|
29
|
+
interface DriveSample {
|
|
30
|
+
xKm: number;
|
|
31
|
+
/** Signal from every mast at this point, in dBm, in the order the masts were given. */
|
|
32
|
+
rssi: number[];
|
|
33
|
+
/** Index of the mast currently holding the call. */
|
|
34
|
+
serving: number;
|
|
35
|
+
}
|
|
36
|
+
interface HandoverEvent {
|
|
37
|
+
atKm: number;
|
|
38
|
+
from: number;
|
|
39
|
+
to: number;
|
|
40
|
+
}
|
|
41
|
+
interface DriveResult {
|
|
42
|
+
samples: DriveSample[];
|
|
43
|
+
events: HandoverEvent[];
|
|
44
|
+
/** The weakest signal the call was actually held on. What the listener hears. */
|
|
45
|
+
worstServingDbm: number;
|
|
46
|
+
/**
|
|
47
|
+
* Times the call actually dropped: a SUSTAINED loss, not a momentary dip.
|
|
48
|
+
*
|
|
49
|
+
* Counting every sample below sensitivity makes a single shadow from a single building look like a
|
|
50
|
+
* dropped call, and then every setting of the margin looks equally broken. A call survives a brief
|
|
51
|
+
* fade; it dies when coverage is gone for long enough that the link cannot be maintained.
|
|
52
|
+
*/
|
|
53
|
+
dropouts: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How far the car must travel with no usable signal before the call is counted as dropped, in km.
|
|
57
|
+
*
|
|
58
|
+
* A hundred metres at city speed is a few seconds of silence, which is where a caller hangs up. Any
|
|
59
|
+
* shorter and a single building's shadow registers as a dropped call; any longer and a genuine
|
|
60
|
+
* coverage hole is scored as a good link.
|
|
61
|
+
*/
|
|
62
|
+
declare const DROP_AFTER_KM = 0.1;
|
|
63
|
+
declare const DEFAULT_DRIVE: Required<Pick<DriveOptions, "shadowDb" | "correlationKm" | "stepKm" | "seed">>;
|
|
64
|
+
/**
|
|
65
|
+
* Drive the road and hand the call over as the rule dictates.
|
|
66
|
+
*
|
|
67
|
+
* The rule is deliberately the real one and nothing more: switch when some neighbour beats the
|
|
68
|
+
* serving mast by `marginDb`. Real networks add a time-to-trigger as well, so a neighbour must stay
|
|
69
|
+
* better for a while before anything happens. Leaving that out keeps ONE control on screen, and the
|
|
70
|
+
* margin alone already produces both failure modes a learner needs to meet.
|
|
71
|
+
*/
|
|
72
|
+
declare function drive(masts: readonly Mast[], options: DriveOptions): DriveResult;
|
|
73
|
+
/**
|
|
74
|
+
* A road with two masts at either end, which is the smallest arrangement that has a boundary.
|
|
75
|
+
*
|
|
76
|
+
* One boundary is all this lesson needs. Adding a third mast produces more handovers without adding
|
|
77
|
+
* an idea, and makes the figure harder to read at the exact place a learner is meant to be looking.
|
|
78
|
+
*/
|
|
79
|
+
declare const TWO_MAST_ROAD: Mast[];
|
|
80
|
+
/**
|
|
81
|
+
* The stretch actually driven, in km, which is NOT the whole road.
|
|
82
|
+
*
|
|
83
|
+
* The masts are eleven kilometres apart so that the midpoint sits near the edge of both cells: any
|
|
84
|
+
* closer and an absurd margin merely degrades the call without ever dropping it, any further and the
|
|
85
|
+
* call drops at every margin, which says nothing about hysteresis.
|
|
86
|
+
*
|
|
87
|
+
* But the drive covers only the five kilometres around the boundary, because that is where the whole
|
|
88
|
+
* phenomenon lives and because of what happens when it is drawn. Shadowing decorrelates over roughly
|
|
89
|
+
* a hundred metres. Across the full eleven kilometres that is one percent of the figure, so the
|
|
90
|
+
* signals render as static and a learner sees noise instead of two curves passing each other. Across
|
|
91
|
+
* five kilometres the same physics reads as visible undulation, and the crossings that cause the
|
|
92
|
+
* ping-pong can actually be counted. A real drive test covers the boundary for the same reason: it
|
|
93
|
+
* is the only part where the answer is in doubt.
|
|
94
|
+
*/
|
|
95
|
+
declare const DRIVE_WINDOW: {
|
|
96
|
+
readonly fromKm: 3.5;
|
|
97
|
+
readonly toKm: 8.5;
|
|
98
|
+
};
|
|
99
|
+
/** How a chosen margin actually performed, in the two terms it trades between. */
|
|
100
|
+
interface MarginVerdict {
|
|
101
|
+
handovers: number;
|
|
102
|
+
worstServingDbm: number;
|
|
103
|
+
dropouts: number;
|
|
104
|
+
/** Plain-language reading of the trade, which is what the learner should leave with. */
|
|
105
|
+
verdict: "ping-pong" | "balanced" | "clinging";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Judge a margin on both of the things it trades between at once.
|
|
109
|
+
*
|
|
110
|
+
* Reporting only the handover count would make a huge margin look perfect, and reporting only the
|
|
111
|
+
* signal would make a margin of zero look perfect. Both numbers together are the only honest summary,
|
|
112
|
+
* and they are what makes the middle findable.
|
|
113
|
+
*/
|
|
114
|
+
declare function judgeMargin(masts: readonly Mast[], options: DriveOptions, runs?: number): MarginVerdict;
|
|
115
|
+
//#endregion
|
|
116
|
+
export { DEFAULT_DRIVE, DRIVE_WINDOW, DROP_AFTER_KM, DriveOptions, DriveResult, DriveSample, HandoverEvent, MarginVerdict, Mast, TWO_MAST_ROAD, drive, judgeMargin };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { HANDSET_SENSITIVITY_DBM, rssiFromMast } from "./cellular.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/networking/models/handover.ts
|
|
4
|
+
/**
|
|
5
|
+
* Handover: how a call survives a moving car.
|
|
6
|
+
*
|
|
7
|
+
* Chapter 2 treats a phone as standing still, which is the one assumption that makes cellular look
|
|
8
|
+
* simple. Everything interesting starts when it moves. A call is held by ONE mast at a time, the
|
|
9
|
+
* phone drives out of that mast's reach, and the call has to be passed to the next one without a gap
|
|
10
|
+
* the listener can hear.
|
|
11
|
+
*
|
|
12
|
+
* The naive rule is "switch to whichever mast is strongest". It is the obvious rule and it fails
|
|
13
|
+
* badly, for a reason worth meeting: near a cell boundary the two signals are almost equal, and
|
|
14
|
+
* shadowing from buildings makes which one is stronger flip back and forth. A phone following that
|
|
15
|
+
* rule hands the call over dozens of times across a few hundred metres. Each handover costs
|
|
16
|
+
* signalling on both masts, and enough of them collapse a network that is otherwise fine. The name
|
|
17
|
+
* for it is the ping-pong effect.
|
|
18
|
+
*
|
|
19
|
+
* The fix is a margin, called hysteresis: do not switch until the neighbour is better by some number
|
|
20
|
+
* of decibels. That kills the ping-pong, and it is not free. A large margin means the phone clings
|
|
21
|
+
* to a mast it has already driven away from, so the signal it is actually holding gets worse and the
|
|
22
|
+
* call gets closer to dropping. There is no setting that is simply correct, which is the real lesson:
|
|
23
|
+
* this is a tuned trade between signalling load and call quality, and a learner should find the
|
|
24
|
+
* middle by overshooting in both directions.
|
|
25
|
+
*
|
|
26
|
+
* The fading here is spatially CORRELATED rather than redrawn at every sample. Shadowing comes from
|
|
27
|
+
* buildings and terrain, so it persists over tens of metres; independent noise per sample would
|
|
28
|
+
* produce a ping-pong that is an artefact of the sampling rate rather than of the physics, and would
|
|
29
|
+
* quietly teach that the effect is worse than it is.
|
|
30
|
+
*/
|
|
31
|
+
/** Deterministic PRNG, because a lab that renders differently on the server than in the browser is broken. */
|
|
32
|
+
function mulberry32(seed) {
|
|
33
|
+
let a = seed >>> 0;
|
|
34
|
+
return () => {
|
|
35
|
+
a = a + 1831565813 >>> 0;
|
|
36
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
37
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
38
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const smoothstep = (t) => t * t * (3 - 2 * t);
|
|
42
|
+
/**
|
|
43
|
+
* Shadow fading along the road: one value per correlation length, smoothly interpolated between.
|
|
44
|
+
*
|
|
45
|
+
* This is what a building casting a radio shadow looks like from a moving car. It matters that it is
|
|
46
|
+
* smooth: the ping-pong this lab teaches must come from two signals genuinely crossing each other
|
|
47
|
+
* several times, not from a value that is redrawn every time we happen to look at it.
|
|
48
|
+
*/
|
|
49
|
+
function shadowField(seed, roadKm, correlationKm, sigmaDb) {
|
|
50
|
+
const random = mulberry32(seed);
|
|
51
|
+
const nodes = [];
|
|
52
|
+
const count = Math.ceil(roadKm / correlationKm) + 3;
|
|
53
|
+
for (let index = 0; index < count; index++) nodes.push((random() + random() + random() - 1.5) * 2 * sigmaDb);
|
|
54
|
+
return (x) => {
|
|
55
|
+
const scaled = Math.max(0, x) / correlationKm;
|
|
56
|
+
const index = Math.min(nodes.length - 2, Math.floor(scaled));
|
|
57
|
+
const t = smoothstep(Math.min(1, Math.max(0, scaled - index)));
|
|
58
|
+
return nodes[index] * (1 - t) + nodes[index + 1] * t;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How far the car must travel with no usable signal before the call is counted as dropped, in km.
|
|
63
|
+
*
|
|
64
|
+
* A hundred metres at city speed is a few seconds of silence, which is where a caller hangs up. Any
|
|
65
|
+
* shorter and a single building's shadow registers as a dropped call; any longer and a genuine
|
|
66
|
+
* coverage hole is scored as a good link.
|
|
67
|
+
*/
|
|
68
|
+
const DROP_AFTER_KM = .1;
|
|
69
|
+
const DEFAULT_DRIVE = {
|
|
70
|
+
shadowDb: 6,
|
|
71
|
+
correlationKm: .12,
|
|
72
|
+
stepKm: .02,
|
|
73
|
+
seed: 20260915
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Drive the road and hand the call over as the rule dictates.
|
|
77
|
+
*
|
|
78
|
+
* The rule is deliberately the real one and nothing more: switch when some neighbour beats the
|
|
79
|
+
* serving mast by `marginDb`. Real networks add a time-to-trigger as well, so a neighbour must stay
|
|
80
|
+
* better for a while before anything happens. Leaving that out keeps ONE control on screen, and the
|
|
81
|
+
* margin alone already produces both failure modes a learner needs to meet.
|
|
82
|
+
*/
|
|
83
|
+
function drive(masts, options) {
|
|
84
|
+
const { shadowDb, correlationKm, stepKm, seed } = {
|
|
85
|
+
...DEFAULT_DRIVE,
|
|
86
|
+
...options
|
|
87
|
+
};
|
|
88
|
+
const fields = masts.map((_, index) => shadowField(seed + index * 7919, options.roadKm, correlationKm, shadowDb));
|
|
89
|
+
const levels = (x) => masts.map((mast, index) => {
|
|
90
|
+
const along = x - mast.atKm;
|
|
91
|
+
const offset = mast.offsetKm ?? .05;
|
|
92
|
+
return rssiFromMast(Math.hypot(along, offset), options.megahertz, options.terrain) + fields[index](x);
|
|
93
|
+
});
|
|
94
|
+
const samples = [];
|
|
95
|
+
const events = [];
|
|
96
|
+
let serving = 0;
|
|
97
|
+
let worstServingDbm = Infinity;
|
|
98
|
+
let dropouts = 0;
|
|
99
|
+
let belowRunKm = 0;
|
|
100
|
+
let counted = false;
|
|
101
|
+
const first = levels(options.fromKm ?? 0);
|
|
102
|
+
serving = first.indexOf(Math.max(...first));
|
|
103
|
+
const startKm = options.fromKm ?? 0;
|
|
104
|
+
for (let x = startKm; x <= options.roadKm + 1e-9; x += stepKm) {
|
|
105
|
+
const rssi = levels(x);
|
|
106
|
+
let challenger = serving;
|
|
107
|
+
for (let index = 0; index < rssi.length; index++) if (index !== serving && rssi[index] > rssi[challenger] + (challenger === serving ? options.marginDb : 0)) challenger = index;
|
|
108
|
+
if (challenger !== serving) {
|
|
109
|
+
events.push({
|
|
110
|
+
atKm: x,
|
|
111
|
+
from: serving,
|
|
112
|
+
to: challenger
|
|
113
|
+
});
|
|
114
|
+
serving = challenger;
|
|
115
|
+
}
|
|
116
|
+
const held = rssi[serving];
|
|
117
|
+
if (held < worstServingDbm) worstServingDbm = held;
|
|
118
|
+
if (held < -100) {
|
|
119
|
+
belowRunKm += stepKm;
|
|
120
|
+
if (!counted && belowRunKm >= .1) {
|
|
121
|
+
dropouts++;
|
|
122
|
+
counted = true;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
belowRunKm = 0;
|
|
126
|
+
counted = false;
|
|
127
|
+
}
|
|
128
|
+
samples.push({
|
|
129
|
+
xKm: x,
|
|
130
|
+
rssi,
|
|
131
|
+
serving
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
samples,
|
|
136
|
+
events,
|
|
137
|
+
worstServingDbm,
|
|
138
|
+
dropouts
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A road with two masts at either end, which is the smallest arrangement that has a boundary.
|
|
143
|
+
*
|
|
144
|
+
* One boundary is all this lesson needs. Adding a third mast produces more handovers without adding
|
|
145
|
+
* an idea, and makes the figure harder to read at the exact place a learner is meant to be looking.
|
|
146
|
+
*/
|
|
147
|
+
const TWO_MAST_ROAD = [{
|
|
148
|
+
id: "west",
|
|
149
|
+
label: "West mast",
|
|
150
|
+
atKm: .5,
|
|
151
|
+
offsetKm: .08
|
|
152
|
+
}, {
|
|
153
|
+
id: "east",
|
|
154
|
+
label: "East mast",
|
|
155
|
+
atKm: 11.5,
|
|
156
|
+
offsetKm: .08
|
|
157
|
+
}];
|
|
158
|
+
/**
|
|
159
|
+
* The stretch actually driven, in km, which is NOT the whole road.
|
|
160
|
+
*
|
|
161
|
+
* The masts are eleven kilometres apart so that the midpoint sits near the edge of both cells: any
|
|
162
|
+
* closer and an absurd margin merely degrades the call without ever dropping it, any further and the
|
|
163
|
+
* call drops at every margin, which says nothing about hysteresis.
|
|
164
|
+
*
|
|
165
|
+
* But the drive covers only the five kilometres around the boundary, because that is where the whole
|
|
166
|
+
* phenomenon lives and because of what happens when it is drawn. Shadowing decorrelates over roughly
|
|
167
|
+
* a hundred metres. Across the full eleven kilometres that is one percent of the figure, so the
|
|
168
|
+
* signals render as static and a learner sees noise instead of two curves passing each other. Across
|
|
169
|
+
* five kilometres the same physics reads as visible undulation, and the crossings that cause the
|
|
170
|
+
* ping-pong can actually be counted. A real drive test covers the boundary for the same reason: it
|
|
171
|
+
* is the only part where the answer is in doubt.
|
|
172
|
+
*/
|
|
173
|
+
const DRIVE_WINDOW = {
|
|
174
|
+
fromKm: 3.5,
|
|
175
|
+
toKm: 8.5
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Judge a margin on both of the things it trades between at once.
|
|
179
|
+
*
|
|
180
|
+
* Reporting only the handover count would make a huge margin look perfect, and reporting only the
|
|
181
|
+
* signal would make a margin of zero look perfect. Both numbers together are the only honest summary,
|
|
182
|
+
* and they are what makes the middle findable.
|
|
183
|
+
*/
|
|
184
|
+
function judgeMargin(masts, options, runs = 5) {
|
|
185
|
+
let handovers = 0;
|
|
186
|
+
let dropouts = 0;
|
|
187
|
+
let worstServingDbm = 0;
|
|
188
|
+
for (let run = 0; run < runs; run++) {
|
|
189
|
+
const result = drive(masts, {
|
|
190
|
+
...options,
|
|
191
|
+
seed: (options.seed ?? DEFAULT_DRIVE.seed) + run * 104729
|
|
192
|
+
});
|
|
193
|
+
handovers += result.events.length;
|
|
194
|
+
dropouts += result.dropouts;
|
|
195
|
+
worstServingDbm += result.worstServingDbm;
|
|
196
|
+
}
|
|
197
|
+
const meanHandovers = handovers / runs;
|
|
198
|
+
const meanDropouts = dropouts / runs;
|
|
199
|
+
const verdict = meanHandovers > 4 ? "ping-pong" : meanDropouts > .5 ? "clinging" : "balanced";
|
|
200
|
+
return {
|
|
201
|
+
handovers: Math.round(meanHandovers),
|
|
202
|
+
worstServingDbm: worstServingDbm / runs,
|
|
203
|
+
dropouts: Math.round(meanDropouts),
|
|
204
|
+
verdict
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
export { DEFAULT_DRIVE, DRIVE_WINDOW, DROP_AFTER_KM, TWO_MAST_ROAD, drive, judgeMargin };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/networking/models/handshake.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Opening a TCP connection, and what it costs before a single byte of yours moves.
|
|
4
|
+
*
|
|
5
|
+
* The three-way handshake is usually taught as a ritual: SYN, SYN-ACK, ACK, learn the order, move on.
|
|
6
|
+
* Taught that way it answers none of the three questions a student actually has, and every one of
|
|
7
|
+
* them has a good answer.
|
|
8
|
+
*
|
|
9
|
+
* WHY THREE. Because each side has a sequence number the other must confirm, and a confirmation can
|
|
10
|
+
* only ride on a message. Two messages confirm one direction. The client learns the server got its
|
|
11
|
+
* number when the SYN-ACK arrives; the server learns nothing until the third message. A two-way
|
|
12
|
+
* handshake leaves the server holding a connection it cannot know the client agreed to, which is
|
|
13
|
+
* also, incidentally, how a SYN flood works.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IT COSTS. One full round trip before the first request can even be sent. On a link with a
|
|
16
|
+
* 200 ms round trip that is 200 ms of nothing, every time, and it is why QUIC and TLS 1.3 went to so
|
|
17
|
+
* much trouble to fold the handshake into fewer trips. A learner who has watched the ladder stretch
|
|
18
|
+
* understands the entire motivation for a decade of protocol design.
|
|
19
|
+
*
|
|
20
|
+
* WHAT HAPPENS WHEN ONE IS LOST. Nothing, for a long time. There is no third party to notice, so the
|
|
21
|
+
* sender waits out a retransmission timeout that starts around a second, which is five times the
|
|
22
|
+
* round trip on a good link and utterly dominates. That asymmetry, a lost packet costing far more
|
|
23
|
+
* than a slow one, is the thing that makes loss and latency different problems.
|
|
24
|
+
*/
|
|
25
|
+
type Side = "client" | "server";
|
|
26
|
+
/** Which segment of the handshake to lose, or none. */
|
|
27
|
+
type DropWhich = "none" | "syn" | "syn-ack" | "ack";
|
|
28
|
+
interface TcpSegment {
|
|
29
|
+
id: string;
|
|
30
|
+
from: Side;
|
|
31
|
+
/** The flags a packet capture would show. */
|
|
32
|
+
flags: string[];
|
|
33
|
+
seq: number;
|
|
34
|
+
ack: number | null;
|
|
35
|
+
/** When it left, in ms from the first attempt. */
|
|
36
|
+
sentMs: number;
|
|
37
|
+
/** When it arrived, or null if it never did. */
|
|
38
|
+
arrivesMs: number | null;
|
|
39
|
+
/** True when this is a second attempt after a timeout. */
|
|
40
|
+
retransmit: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface HandshakeResult {
|
|
43
|
+
segments: TcpSegment[];
|
|
44
|
+
/** When the client may send its first request, in ms. */
|
|
45
|
+
readyMs: number;
|
|
46
|
+
/** When the server considers the connection open, in ms. */
|
|
47
|
+
serverOpenMs: number;
|
|
48
|
+
/** Time lost purely to waiting out a timeout. */
|
|
49
|
+
timeoutCostMs: number;
|
|
50
|
+
/** State of each side over time, for the column labels. */
|
|
51
|
+
states: {
|
|
52
|
+
side: Side;
|
|
53
|
+
atMs: number;
|
|
54
|
+
state: string;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
interface HandshakeOptions {
|
|
58
|
+
/** Round trip time of the link, in ms. */
|
|
59
|
+
rttMs: number;
|
|
60
|
+
drop?: DropWhich;
|
|
61
|
+
/** Initial retransmission timeout, in ms. One second is the specified starting value. */
|
|
62
|
+
rtoMs?: number;
|
|
63
|
+
}
|
|
64
|
+
/** The specified initial retransmission timeout. Deliberately long, and the reason loss hurts. */
|
|
65
|
+
declare const DEFAULT_RTO_MS = 1000;
|
|
66
|
+
/**
|
|
67
|
+
* Run the handshake and return every segment with the time it left and the time it landed.
|
|
68
|
+
*
|
|
69
|
+
* Times are exact rather than simulated tick by tick, because the whole point is the arithmetic: one
|
|
70
|
+
* way is half a round trip, so the client is ready after one full round trip and the server half a
|
|
71
|
+
* trip later. Drawing that from a clock rather than from a formula would invite the reader to think
|
|
72
|
+
* the numbers came from somewhere mysterious.
|
|
73
|
+
*/
|
|
74
|
+
declare function handshake({
|
|
75
|
+
rttMs,
|
|
76
|
+
drop,
|
|
77
|
+
rtoMs
|
|
78
|
+
}: HandshakeOptions): HandshakeResult;
|
|
79
|
+
/** Total time before the first response could come back: the handshake, then the request and reply. */
|
|
80
|
+
declare const firstResponseMs: (result: HandshakeResult, rttMs: number) => number;
|
|
81
|
+
interface DropSpec {
|
|
82
|
+
id: DropWhich;
|
|
83
|
+
label: string;
|
|
84
|
+
/** Who notices the loss, which is the interesting part. */
|
|
85
|
+
noticedBy: string;
|
|
86
|
+
note: string;
|
|
87
|
+
}
|
|
88
|
+
declare const DROPS: Record<DropWhich, DropSpec>;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { DEFAULT_RTO_MS, DROPS, DropSpec, DropWhich, HandshakeOptions, HandshakeResult, Side, TcpSegment, firstResponseMs, handshake };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/networking/models/handshake.ts
|
|
2
|
+
/** The specified initial retransmission timeout. Deliberately long, and the reason loss hurts. */
|
|
3
|
+
const DEFAULT_RTO_MS = 1e3;
|
|
4
|
+
/** Sequence numbers are random in practice; fixed here so the figure is readable and stable. */
|
|
5
|
+
const CLIENT_ISN = 1e3;
|
|
6
|
+
const SERVER_ISN = 5e3;
|
|
7
|
+
/**
|
|
8
|
+
* Run the handshake and return every segment with the time it left and the time it landed.
|
|
9
|
+
*
|
|
10
|
+
* Times are exact rather than simulated tick by tick, because the whole point is the arithmetic: one
|
|
11
|
+
* way is half a round trip, so the client is ready after one full round trip and the server half a
|
|
12
|
+
* trip later. Drawing that from a clock rather than from a formula would invite the reader to think
|
|
13
|
+
* the numbers came from somewhere mysterious.
|
|
14
|
+
*/
|
|
15
|
+
function handshake({ rttMs, drop = "none", rtoMs = DEFAULT_RTO_MS }) {
|
|
16
|
+
const hop = rttMs / 2;
|
|
17
|
+
const segments = [];
|
|
18
|
+
const states = [{
|
|
19
|
+
side: "client",
|
|
20
|
+
atMs: 0,
|
|
21
|
+
state: "CLOSED"
|
|
22
|
+
}, {
|
|
23
|
+
side: "server",
|
|
24
|
+
atMs: 0,
|
|
25
|
+
state: "LISTEN"
|
|
26
|
+
}];
|
|
27
|
+
let timeoutCostMs = 0;
|
|
28
|
+
let clock = 0;
|
|
29
|
+
const send = (id, from, flags, seq, ack, sentMs, lost, retransmit = false) => {
|
|
30
|
+
const segment = {
|
|
31
|
+
id,
|
|
32
|
+
from,
|
|
33
|
+
flags,
|
|
34
|
+
seq,
|
|
35
|
+
ack,
|
|
36
|
+
sentMs,
|
|
37
|
+
arrivesMs: lost ? null : sentMs + hop,
|
|
38
|
+
retransmit
|
|
39
|
+
};
|
|
40
|
+
segments.push(segment);
|
|
41
|
+
return segment;
|
|
42
|
+
};
|
|
43
|
+
let syn = send("syn", "client", ["SYN"], CLIENT_ISN, null, clock, drop === "syn");
|
|
44
|
+
states.push({
|
|
45
|
+
side: "client",
|
|
46
|
+
atMs: clock,
|
|
47
|
+
state: "SYN-SENT"
|
|
48
|
+
});
|
|
49
|
+
if (drop === "syn") {
|
|
50
|
+
clock += rtoMs;
|
|
51
|
+
timeoutCostMs += rtoMs;
|
|
52
|
+
syn = send("syn-2", "client", ["SYN"], CLIENT_ISN, null, clock, false, true);
|
|
53
|
+
}
|
|
54
|
+
const synArrives = syn.arrivesMs;
|
|
55
|
+
states.push({
|
|
56
|
+
side: "server",
|
|
57
|
+
atMs: synArrives,
|
|
58
|
+
state: "SYN-RECEIVED"
|
|
59
|
+
});
|
|
60
|
+
let synAck = send("syn-ack", "server", ["SYN", "ACK"], SERVER_ISN, 1001, synArrives, drop === "syn-ack");
|
|
61
|
+
if (drop === "syn-ack") {
|
|
62
|
+
const retryAt = syn.sentMs + rtoMs;
|
|
63
|
+
timeoutCostMs += retryAt - syn.sentMs;
|
|
64
|
+
synAck = send("syn-ack-2", "server", ["SYN", "ACK"], SERVER_ISN, 1001, send("syn-2", "client", ["SYN"], CLIENT_ISN, null, retryAt, false, true).arrivesMs, false, true);
|
|
65
|
+
}
|
|
66
|
+
const synAckArrives = synAck.arrivesMs;
|
|
67
|
+
states.push({
|
|
68
|
+
side: "client",
|
|
69
|
+
atMs: synAckArrives,
|
|
70
|
+
state: "ESTABLISHED"
|
|
71
|
+
});
|
|
72
|
+
let ack = send("ack", "client", ["ACK"], 1001, 5001, synAckArrives, drop === "ack");
|
|
73
|
+
if (drop === "ack") {
|
|
74
|
+
const serverRetryAt = synAck.sentMs + rtoMs;
|
|
75
|
+
timeoutCostMs += serverRetryAt - synAck.sentMs;
|
|
76
|
+
ack = send("ack-2", "client", ["ACK"], 1001, 5001, send("syn-ack-2", "server", ["SYN", "ACK"], SERVER_ISN, 1001, serverRetryAt, false, true).arrivesMs, false, true);
|
|
77
|
+
}
|
|
78
|
+
const ackArrives = ack.arrivesMs;
|
|
79
|
+
states.push({
|
|
80
|
+
side: "server",
|
|
81
|
+
atMs: ackArrives,
|
|
82
|
+
state: "ESTABLISHED"
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
segments,
|
|
86
|
+
readyMs: synAckArrives,
|
|
87
|
+
serverOpenMs: ackArrives,
|
|
88
|
+
timeoutCostMs,
|
|
89
|
+
states
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Total time before the first response could come back: the handshake, then the request and reply. */
|
|
93
|
+
const firstResponseMs = (result, rttMs) => result.readyMs + rttMs;
|
|
94
|
+
const DROPS = {
|
|
95
|
+
none: {
|
|
96
|
+
id: "none",
|
|
97
|
+
label: "Nothing lost",
|
|
98
|
+
noticedBy: "nobody",
|
|
99
|
+
note: "Three segments, one and a half round trips, and the client may send its request as the SYN-ACK arrives."
|
|
100
|
+
},
|
|
101
|
+
syn: {
|
|
102
|
+
id: "syn",
|
|
103
|
+
label: "The SYN is lost",
|
|
104
|
+
noticedBy: "the client, after a timeout",
|
|
105
|
+
note: "The server never knew a connection was attempted. Only the client can notice, and it has nothing to go on but a clock."
|
|
106
|
+
},
|
|
107
|
+
"syn-ack": {
|
|
108
|
+
id: "syn-ack",
|
|
109
|
+
label: "The SYN-ACK is lost",
|
|
110
|
+
noticedBy: "the client, after a timeout",
|
|
111
|
+
note: "The server is now holding a half-open connection it thinks is progressing. The client sees only silence and retries its SYN."
|
|
112
|
+
},
|
|
113
|
+
ack: {
|
|
114
|
+
id: "ack",
|
|
115
|
+
label: "The final ACK is lost",
|
|
116
|
+
noticedBy: "the server, after a timeout",
|
|
117
|
+
note: "The client already believes it is connected and may start sending. The server does not, and re-sends its SYN-ACK until it hears something."
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { DEFAULT_RTO_MS, DROPS, firstResponseMs, handshake };
|